mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
SLC Fixes
This commit is contained in:
@@ -35,6 +35,7 @@ class CarState(CarStateBase):
|
||||
self.lc_button = 0
|
||||
self.cancel_button = False
|
||||
self.cancel_resume_button = FordStockCruiseButton()
|
||||
self.cruise_buttons = {ButtonType.accelCruise: False, ButtonType.decelCruise: False}
|
||||
self.steering_angle_offset_deg = 0.0
|
||||
self.lkas_available = False
|
||||
self.lkas_available_ts_nanos = 0
|
||||
@@ -169,6 +170,13 @@ class CarState(CarStateBase):
|
||||
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
button_events += create_button_events(self.cancel_button, prev_cancel_button, {1: ButtonType.cancel})
|
||||
for button, signals in (
|
||||
(ButtonType.accelCruise, ('CcAslButtnSetIncPress', 'CcAslButtnResIncPress', 'CcAslButtnIndxIncPress')),
|
||||
(ButtonType.decelCruise, ('CcAslButtnSetDecPress', 'CcAslButtnResDecPress', 'CcAslButtnIndxDecPress')),
|
||||
):
|
||||
pressed = any(self.buttons_stock_values[signal] for signal in signals)
|
||||
button_events += create_button_events(pressed, self.cruise_buttons[button], {1: button})
|
||||
self.cruise_buttons[button] = pressed
|
||||
|
||||
ret.buttonEvents = button_events
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Original switch context, state event and reached controller regressions."""
|
||||
import pytest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs, gen_empty_fingerprint
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
@@ -39,6 +41,27 @@ def test_actual_parser_cancel_edges_are_op_long_only_and_contextual():
|
||||
assert cancel == (expected if op_long else [])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('vehicle', [CAR.FORD_ESCAPE_MK4, CAR.FORD_MUSTANG_MACH_E_MK1])
|
||||
@pytest.mark.parametrize('op_long', [False, True])
|
||||
@pytest.mark.parametrize('signal,button', [
|
||||
('CcAslButtnSetIncPress', 'accelCruise'), ('CcAslButtnResIncPress', 'accelCruise'),
|
||||
('CcAslButtnIndxIncPress', 'accelCruise'), ('CcAslButtnSetDecPress', 'decelCruise'),
|
||||
('CcAslButtnResDecPress', 'decelCruise'), ('CcAslButtnIndxDecPress', 'decelCruise'),
|
||||
])
|
||||
def test_directional_cruise_edges_reach_software_cruise_only_with_op_long(vehicle, op_long, signal, button):
|
||||
cp = params(vehicle, alpha=op_long)
|
||||
state = CarState(cp)
|
||||
parsers = state.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for i, (pressed, expected) in enumerate(((0, []), (1, [True]), (1, []), (0, [False]))):
|
||||
parsers[Bus.pt].update([(1_000_000_000 + i * 10_000_000, [
|
||||
packer.make_can_msg('Steering_Data_FD1', parsers[Bus.pt].bus, {signal: pressed}),
|
||||
])])
|
||||
out = state.update(parsers)
|
||||
edges = [event.pressed for event in out.buttonEvents if event.type == getattr(structs.CarState.ButtonEvent.Type, button)]
|
||||
assert edges == (expected if op_long else [])
|
||||
|
||||
|
||||
def test_reached_controller_preserves_precedence_cadence_and_stock_bits():
|
||||
cp = params(CAR.FORD_MUSTANG_MACH_E_MK1, alpha=False)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
|
||||
@@ -261,7 +261,8 @@ class VCruiseHelper:
|
||||
target_kph = round(target_mps * CV.MS_TO_KPH)
|
||||
else:
|
||||
target_kph = round(target_mps * CV.MS_TO_MPH) * CV.MPH_TO_KPH
|
||||
self.v_cruise_kph = float(np.clip(target_kph, V_CRUISE_MIN, V_CRUISE_MAX))
|
||||
# Match ordinary button precision so stepping away and back restores this target.
|
||||
self.v_cruise_kph = float(np.clip(round(target_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX))
|
||||
self.v_cruise_cluster_kph = self.v_cruise_kph
|
||||
|
||||
def initialize_v_cruise(self, CS, experimental_mode: bool, *, resume: bool | None = None, initial_speed: object = V_CRUISE_INITIAL) -> None:
|
||||
|
||||
@@ -21,7 +21,8 @@ def test_real_absolute_request_changes_software_speed_without_physical_button(me
|
||||
assert sender.dispatch(True, sm, params, publisher, now_ns=now, target_mps=target)
|
||||
assert str(publisher.message.slcAction.kind) == 'cruiseSet'
|
||||
assert consumer.apply(publisher.message, params, state, sm, helper, now_ns=now, is_metric=metric, enabled=True)
|
||||
assert helper.v_cruise_kph == pytest.approx(value if metric else value * CV.MPH_TO_KPH)
|
||||
assert helper.v_cruise_kph == pytest.approx(value if metric else round(value * CV.MPH_TO_KPH, 1))
|
||||
assert round(helper.v_cruise_kph * (1 if metric else CV.KPH_TO_MPH)) == value
|
||||
assert not list(state.buttonEvents)
|
||||
assert not consumer.apply(publisher.message, params, state, sm, helper, now_ns=now + 100_000_000,
|
||||
is_metric=metric, enabled=True)
|
||||
|
||||
@@ -528,12 +528,8 @@ vm.state.selected = "current_speed"
|
||||
vm.setSpeedSources("speedSources", true)
|
||||
assert.notEqual(vm.state.draft.speedSources, true) // Only from the cruise widget's own panel.
|
||||
vm.state.selected = "cruise_limits"
|
||||
vm.setSpeedAlign("center")
|
||||
assert.equal(vm.state.draft.speedAlign, "center")
|
||||
vm.setSpeedAlign("justify")
|
||||
assert.equal(vm.state.draft.speedAlign, "center") // Only left, center or right.
|
||||
vm.undo()
|
||||
assert.equal(vm.state.draft.speedAlign ?? "left", "left")
|
||||
assert.doesNotMatch(OnroadLayoutPage.template, /speedAlign|Text alignment/)
|
||||
assert.equal(OnroadLayoutPage.methods.setSpeedAlign, undefined)
|
||||
vm.setSpeedSources("palette", true)
|
||||
assert.equal(vm.state.draft.palette === true, false) // Only the two sources settings.
|
||||
// Moving or removing widgets keeps the same backdrop; scene and size changes replace it.
|
||||
@@ -715,7 +711,9 @@ function feedFixture() {
|
||||
const normal = feedFixture()
|
||||
normal.feed.start()
|
||||
assert.equal(normal.requests[0].url, "./api/ui/layout")
|
||||
await normal.reply(0)
|
||||
const legacyAlignment = snapshot()
|
||||
legacyAlignment.document.speedAlign = "right"
|
||||
await normal.reply(0, legacyAlignment)
|
||||
const draft = copy(normal.feed.data.document); draft.layouts.compact.max_speed.x = 80
|
||||
normal.feed.save(draft)
|
||||
normal.feed.load(); normal.feed.save(draft)
|
||||
@@ -724,6 +722,7 @@ assert.deepEqual(JSON.parse(normal.requests[1].options.body), { revision: "saved
|
||||
assert.equal(normal.requests[1].options.credentials, "same-origin")
|
||||
await normal.reply(1, { ...snapshot(), document: draft, revision: "new-revision" })
|
||||
assert.equal(normal.updates.at(-1).draft.layouts.compact.max_speed.x, 80)
|
||||
assert.equal(normal.updates.at(-1).draft.speedAlign, "right") // Unrelated edits preserve legacy layout fields.
|
||||
assert.equal(normal.feed.data.revision, "new-revision")
|
||||
|
||||
for (const status of [409, 503, 400]) {
|
||||
|
||||
@@ -21,7 +21,7 @@ metadata.widgets.car_exit = { label: "Exit to car", kind: "car_exit", width: 96,
|
||||
metadata.widgetOrder = [...(metadata.widgetOrder || Object.keys(native.metadata.profiles.large.widgets)), "car_exit"]
|
||||
const widgets = Object.fromEntries(Object.entries(metadata.widgets).map(([id, widget]) => [id,
|
||||
{ ...widget.default, ...(widget.resizable ? { size: widget.resizable.default } : {}) }]))
|
||||
const doc = { version: 4, clock24Hour: false, largeUiGammaTrial: false, blindSpotBorder: false, signalBorder: false, blindSpotCamera: false, hidePersonalityAlerts: false, wheelPedalFeedback: false, leadIndicator: false, leadInfo: "Off", speedSources: false, speedSourcesCard: false, speedAlign: "left", canvas: { width: 2880, height: 1080 }, widgets }
|
||||
const doc = { version: 4, clock24Hour: false, largeUiGammaTrial: false, blindSpotBorder: false, signalBorder: false, blindSpotCamera: false, hidePersonalityAlerts: false, wheelPedalFeedback: false, leadIndicator: false, leadInfo: "Off", speedSources: false, speedSourcesCard: false, speedAlign: "center", canvas: { width: 2880, height: 1080 }, widgets }
|
||||
const raw = { version: 1, document: doc, defaults: copy(doc), metadata,
|
||||
screen: { width: 1280, height: 720, margin_width: 0, margin_height: 240 },
|
||||
revision: "aa-screen-and-layout", editable: true, valid: true,
|
||||
@@ -29,6 +29,7 @@ const raw = { version: 1, document: doc, defaults: copy(doc), metadata,
|
||||
const data = editorSnapshot(raw)
|
||||
assert.equal(validSnapshot(data), true)
|
||||
assert.equal(validDocument(data.document, data.metadata), true)
|
||||
assert.equal(data.document.speedAlign, "center") // Legacy preferences still load after removing the editor control.
|
||||
assert.equal(data.document.layouts.large.car_exit.opacity, 75)
|
||||
assert.equal(data.document.layouts.large.car_exit.background, false)
|
||||
const resizedMonitor = copy(data.document)
|
||||
@@ -236,6 +237,7 @@ assert.equal(requests[0].url, "./api/android-auto/layout")
|
||||
assert.equal(requests[1].body.document.version, 4)
|
||||
assert.deepEqual(Object.keys(requests[1].body.document).sort(), ["blindSpotBorder", "blindSpotCamera", "canvas", "clock24Hour", "hidePersonalityAlerts", "largeUiGammaTrial", "leadIndicator", "leadInfo", "signalBorder", "speedAlign", "speedSources", "speedSourcesCard", "version", "wheelPedalFeedback", "widgets"])
|
||||
assert.equal(requests[1].body.document.widgets.current_speed.x, 1170)
|
||||
assert.equal(requests[1].body.document.speedAlign, "center")
|
||||
assert.equal(updates.at(-1).notice, "Android Auto layout saved.")
|
||||
feed.stop()
|
||||
console.log("Projection editor: isolated document, strict snapshot, routing confirmation, actual Vue template, endpoint and placement-only save passed")
|
||||
|
||||
@@ -105,9 +105,9 @@ export function previewPoint(event, rect, profile) {
|
||||
}
|
||||
}
|
||||
|
||||
// Android Auto display switches, independent of the comma screen's preferences.
|
||||
// Text alignment inside the cruise and speed-limit card, matching onroad_customization.SPEED_ALIGNMENTS.
|
||||
// Legacy saved layouts retain this field even though the paired speed widget uses fixed alignment.
|
||||
export const SPEED_ALIGNMENTS = ["left", "center", "right"]
|
||||
// Android Auto display switches, independent of the comma screen's preferences.
|
||||
export const PROJECTION_DISPLAY_SWITCHES = ["blindSpotBorder", "signalBorder", "blindSpotCamera", "hidePersonalityAlerts", "wheelPedalFeedback", "leadIndicator"]
|
||||
|
||||
export function validDocument(document, metadata) {
|
||||
|
||||
@@ -229,15 +229,6 @@ export const ONROAD_LAYOUT_TEMPLATE = `
|
||||
<p v-if="selectedWidget.defaultAnchor === 'driver_side'" class="gx-note">The default position follows the driver’s side. A moved position stays where you place it.</p>
|
||||
<p v-if="selectedWidget.note" class="gx-note">{{ selectedWidget.note }}</p>
|
||||
<label v-if="selectedWidget.kind === 'clock'" class="gx-layout__clock-format"><input type="checkbox" :checked="!!state.draft.clock24Hour" :disabled="!editAvailable || !!state.drag || !!state.layerDrag" @change="setClock24Hour"> Use 24-hour time</label>
|
||||
<div v-if="selectedWidget.kind === 'cruise_limits'" class="gx-layout__favorite-display">
|
||||
<span>Text alignment</span>
|
||||
<div class="gx-segment" role="group" aria-label="Cruise and speed limit text alignment">
|
||||
<button v-for="align in ['left', 'center', 'right']" :key="align" class="gx-btn" type="button"
|
||||
:class="(state.draft.speedAlign || 'left') === align ? 'gx-btn--primary' : 'gx-btn--tonal'"
|
||||
:aria-pressed="(state.draft.speedAlign || 'left') === align" :disabled="!editAvailable || !!state.drag || !!state.layerDrag"
|
||||
@click="setSpeedAlign(align)">{{ align === 'left' ? 'Left' : align === 'center' ? 'Center' : 'Right' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<GalaxySettingRow v-if="selectedWidget.kind === 'cruise_limits'" :row="switchRow('Show Speed-Limit Sources', state.draft.speedSources === true)" :index="0"
|
||||
:disabled="!editAvailable || !!state.drag || !!state.layerDrag" :save-value="(index, value) => setSpeedSources('speedSources', value === 'On')" />
|
||||
<GalaxySettingRow v-if="selectedWidget.kind === 'cruise_limits' && state.draft.speedSources === true" :row="switchRow('Show as Separate Card', state.draft.speedSourcesCard === true)" :index="1"
|
||||
|
||||
@@ -851,14 +851,6 @@ export const OnroadLayoutPage = {
|
||||
this.recordChange(before)
|
||||
this.state.notice = ""
|
||||
},
|
||||
setSpeedAlign(align) {
|
||||
if (!this.editable || this.state.drag || this.state.layerDrag || this.selectedWidget?.kind !== "cruise_limits" ||
|
||||
!["left", "center", "right"].includes(align)) return
|
||||
const before = clone(this.state.draft)
|
||||
this.state.draft.speedAlign = align
|
||||
this.recordChange(before)
|
||||
this.state.notice = ""
|
||||
},
|
||||
setLargeUiGammaTrial(value) {
|
||||
if (!this.projection || !this.editable || this.state.drag || this.state.layerDrag) return
|
||||
const before = clone(this.state.draft)
|
||||
|
||||
@@ -42,7 +42,8 @@ limit is qualified again.
|
||||
A qualified manual change to a different set speed marks an override, for increases
|
||||
and decreases. An X crosses the sign while override is active. Returning to the
|
||||
applied set speed clears it, using card's receipt to account for whole-unit rounding. Adopting
|
||||
a new limit also clears it. New-limit decisions continue during override.
|
||||
a new limit also clears it. Adoption retains the ordinary buttons' 0.1 km/h internal
|
||||
precision, including for mph targets. New-limit decisions continue during override.
|
||||
|
||||
The accelerator temporarily allows higher vehicle speed without changing set speed
|
||||
or creating an SLC override. Adoption and confirmation can update software set
|
||||
@@ -74,6 +75,8 @@ qualified planner presentation. A command receipt distinguishes application from
|
||||
limit acceptance. Rejected or expired commands may retry with current evidence
|
||||
while the accepted limit and expected set speed remain current, with no pending
|
||||
decision or manual override. Applied commands are not repeated.
|
||||
Declining or timing out a returned limit never revives an earlier adoption command;
|
||||
the historical adoption remains recorded independently of the new rejection.
|
||||
|
||||
Transport changes append to reserved custom messages. `SlcState` appends automatic
|
||||
countdown metadata at ordinals 51–52 and latest-received display metadata at 53–55.
|
||||
|
||||
@@ -144,6 +144,7 @@ class AdoptionProposal:
|
||||
class State:
|
||||
session_id: str
|
||||
observation: Observation = Observation(ObservationKind.ABSENT)
|
||||
# A historical adoption may match a returned proposal the driver now declines.
|
||||
accepted: AcceptedLimit | None = None
|
||||
pending: PendingDecision | None = None
|
||||
rejected: frozenset[Candidate] = frozenset()
|
||||
@@ -273,8 +274,6 @@ def _state_errors(state: State) -> list[str]:
|
||||
all(_candidate_valid(c, state.session_id) for c in state.rejected))
|
||||
if not rejections_valid:
|
||||
errors.append("invalid rejection history")
|
||||
elif state.accepted is not None and _accepted_valid(state.accepted) and state.accepted.candidate in state.rejected:
|
||||
errors.append("accepted candidate is also rejected")
|
||||
if state.pending is not None:
|
||||
pending = state.pending
|
||||
if (not isinstance(pending, PendingDecision) or not _candidate_valid(pending.candidate) or
|
||||
@@ -413,7 +412,7 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
|
||||
adopt_receipt = replace(adopt_receipt, status="different_presentation")
|
||||
else:
|
||||
# Recorded for any authority; only an owner of longitudinal control commands a target.
|
||||
state, history = _accept(replace(state, rejected=state.rejected - {candidate}), candidate, now_ns)
|
||||
state, history = _accept(replace(state, rejected=frozenset(c for c in state.rejected if c.speed_mps != candidate.speed_mps)), candidate, now_ns)
|
||||
assert state.accepted is not None
|
||||
adopt_receipt = replace(adopt_receipt, consumed=True, status="adopt")
|
||||
if not authority.can_control:
|
||||
|
||||
@@ -430,7 +430,8 @@ class Runtime:
|
||||
def _command(self, result: comp.Result, sm, now_ns: int, old_pending,
|
||||
action: acc.DriverAction | None, adopt: acc.AdoptRequest | None):
|
||||
decision = result.acceptance
|
||||
if (decision is None or decision.state.accepted is None or decision.state.pending is not None or decision.authority is None or
|
||||
if (decision is None or decision.basis not in ('accepted', 'driver_accept', 'driver_adopt') or
|
||||
decision.state.accepted is None or decision.state.pending is not None or decision.authority is None or
|
||||
not decision.authority.can_control or result.coordinate is None or
|
||||
result.domain is None or result.domain.context is None or result.selection is None or
|
||||
result.selection.observation.candidate is None or
|
||||
|
||||
@@ -423,17 +423,25 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertIsNone(result.history_write)
|
||||
|
||||
def test_accepted_or_pending_candidate_cannot_also_be_rejected(self):
|
||||
accepted_conflict = replace(saved(), rejected=frozenset({HIGH}))
|
||||
pending_conflict = replace(self.pending().state, rejected=frozenset({LOW}))
|
||||
for state in (accepted_conflict, pending_conflict):
|
||||
with self.subTest(state=state):
|
||||
result = step(state, valid(HIGH), ACTIVE, Policy(False, False, True), now_ns=NS)
|
||||
self.assertTrue(result.errors)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertIsNone(result.history_write)
|
||||
self.assertIs(result.state, state)
|
||||
self.assertEqual(result.state.accepted.candidate, HIGH)
|
||||
def test_pending_candidate_cannot_also_be_rejected(self):
|
||||
state = replace(self.pending().state, rejected=frozenset({LOW}))
|
||||
result = step(state, valid(HIGH), ACTIVE, Policy(False, False, True), now_ns=NS)
|
||||
self.assertTrue(result.errors)
|
||||
self.assertIsNone(result.control_target_mps)
|
||||
self.assertIsNone(result.history_write)
|
||||
self.assertIs(result.state, state)
|
||||
self.assertEqual(result.state.accepted.candidate, HIGH)
|
||||
|
||||
def test_rejecting_a_returned_limit_preserves_its_earlier_accepted_history(self):
|
||||
other = self.pending()
|
||||
returned = self.pending(candidate=HIGH, state=other.state, now_ns=NS)
|
||||
declined = self.act(returned.state, ActionKind.REJECT, now_ns=2 * NS)
|
||||
held = step(declined.state, valid(HIGH), ACTIVE, Policy(), now_ns=3 * NS)
|
||||
self.assertFalse(held.errors)
|
||||
self.assertEqual(held.basis, 'rejected')
|
||||
self.assertEqual(held.state.accepted, returned.state.accepted)
|
||||
self.assertIn(HIGH, held.state.rejected)
|
||||
self.assertIsNone(held.state.pending)
|
||||
|
||||
def test_changed_limits_do_not_accumulate_obsolete_rejections(self):
|
||||
state = saved()
|
||||
@@ -538,6 +546,18 @@ class TestAcceptance(unittest.TestCase):
|
||||
self.assertTrue(result.adoption.clear_override)
|
||||
self.assertEqual(result.adoption.reconciliation_speed_mps, HIGH.speed_mps)
|
||||
|
||||
def test_explicit_adopt_clears_same_speed_rejection_after_provider_change(self):
|
||||
rejected = self.act(self.pending().state, ActionKind.REJECT)
|
||||
refreshed = replace(LOW, source='map', observation_identity=geographic('refreshed'))
|
||||
shown = step(rejected.state, valid(refreshed), ACTIVE, Policy(), now_ns=2 * NS)
|
||||
adopted = step(shown.state, valid(refreshed), ACTIVE, Policy(), now_ns=3 * NS,
|
||||
adopt=self.adopt_request(shown.state, sequence=1))
|
||||
self.assertTrue(adopted.adoption_receipt.consumed)
|
||||
self.assertFalse(adopted.state.rejected)
|
||||
held = step(adopted.state, valid(refreshed), ACTIVE, Policy(), now_ns=4 * NS)
|
||||
self.assertFalse(held.errors)
|
||||
self.assertEqual(held.basis, 'accepted')
|
||||
|
||||
def test_adopt_precedes_confirmation_timeout(self):
|
||||
pending = self.pending()
|
||||
result = step(pending.state, valid(LOW), ACTIVE, Policy(), now_ns=CONFIRMATION_NS, adopt=self.adopt_request(pending.state))
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
|
||||
from opendbc.car.dashboard_speed_limit import Status
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||
from openpilot.selfdrive.controls.lib import longitudinal_planner as planner
|
||||
from openpilot.starpilot.speed_limits import acceptance as acc, physical_actions
|
||||
@@ -95,6 +96,34 @@ def test_expired_adoption_does_not_retry_after_driver_or_authority_changes(drive
|
||||
assert tick(runtime, sm, cp, 3.2, **kwargs).command is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure', ['rejected', 'expired'])
|
||||
@pytest.mark.parametrize('response', ['reject', 'timeout'])
|
||||
def test_returned_limit_cannot_retry_an_older_adoption_after_decline(drive, failure, response):
|
||||
sm, cp = drive
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCConfirmation': True}))
|
||||
pending = tick(runtime, sm, cp, 0).message.slcState
|
||||
first = tick(runtime, sm, cp, 1, request=Action(runtime.session_id, 1, pending.decisionId, pending.presentationId, 'accept'))
|
||||
command = first.command.slcCruiseCommand
|
||||
sm['slcDashboardObservation'].speedMps = 20.
|
||||
receipt = command_receipt(runtime, command, 1.05, 60., 60., applied=False) if failure == 'rejected' else None
|
||||
tick(runtime, sm, cp, 1.05, cruise_event=receipt)
|
||||
sm['slcDashboardObservation'].speedMps = 25.
|
||||
pending = tick(runtime, sm, cp, 4).message.slcState
|
||||
action = Action(runtime.session_id, 2, pending.decisionId, pending.presentationId, 'reject') if response == 'reject' else None
|
||||
seconds = 5 if action else 14
|
||||
declined = tick(runtime, sm, cp, seconds, request=action)
|
||||
assert declined.result.acceptance.basis == ('driver_reject' if action else 'timeout_reject')
|
||||
assert declined.command is None
|
||||
held = tick(runtime, sm, cp, seconds + .1)
|
||||
assert not held.result.errors and not runtime.state.reset_required and held.command is None
|
||||
assert sm['carState'].vCruise == 60.
|
||||
# An explicit later adoption remains available after declining the new decision.
|
||||
current = declined.message.slcState
|
||||
readopted = tick(runtime, sm, cp, seconds + .2,
|
||||
request=Action(runtime.session_id, 3, 0, current.presentationId, 'adopt'))
|
||||
assert readopted.command is not None and readopted.command.slcCruiseCommand.targetMps == 25.
|
||||
|
||||
|
||||
@pytest.mark.parametrize('limit,offset,bounded', [(140., 10., 145.), (10., -5., 8.)])
|
||||
@pytest.mark.parametrize('metric', [False, True])
|
||||
def test_adoption_and_display_use_existing_cruise_bounds(drive, limit, offset, bounded, metric):
|
||||
@@ -120,30 +149,37 @@ def test_adoption_and_display_use_existing_cruise_bounds(drive, limit, offset, b
|
||||
assert 8 <= helper.v_cruise_kph <= 145
|
||||
|
||||
|
||||
def test_manual_override_compares_against_the_applied_rounded_set_speed(drive):
|
||||
@pytest.mark.parametrize('metric,display', [(True, 72), (False, 55), (False, 60), (False, 65), (False, 70)])
|
||||
@pytest.mark.parametrize('direction', [-1, 1])
|
||||
def test_manual_override_compares_against_the_applied_rounded_set_speed(drive, metric, display, direction):
|
||||
sm, cp = drive
|
||||
cp.pcmCruise = False
|
||||
sm['slcDashboardObservation'].speedMps = 45 * .44704
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'IsMetric': True}))
|
||||
sm['slcDashboardObservation'].speedMps = 45 * CV.MPH_TO_MS if metric else display * CV.MPH_TO_MS
|
||||
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'IsMetric': metric}))
|
||||
tick(runtime, sm, cp, 0)
|
||||
adopted = tick(runtime, sm, cp, 3)
|
||||
helper = VCruiseHelper(cp)
|
||||
helper.apply_slc_target(adopted.command.slcCruiseCommand.targetMps, True)
|
||||
assert helper.v_cruise_kph == 72.
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = 72.
|
||||
tick(runtime, sm, cp, 3.05, cruise_event=command_receipt(runtime, adopted.command.slcCruiseCommand, 3.05, 60., 72.))
|
||||
for event_id, (previous, chosen) in enumerate(((72., 73.), (73., 74.), (74., 73.), (73., 72.)), 2):
|
||||
helper.adjust_v_cruise(car.CarState.ButtonEvent.Type.accelCruise if chosen > previous else
|
||||
car.CarState.ButtonEvent.Type.decelCruise, sm['carState'], True)
|
||||
assert helper.v_cruise_kph == chosen
|
||||
helper.apply_slc_target(adopted.command.slcCruiseCommand.targetMps, metric)
|
||||
baseline = helper.v_cruise_kph
|
||||
conversion = 1 if metric else CV.KPH_TO_MPH
|
||||
assert round(baseline * conversion) == display
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = baseline
|
||||
tick(runtime, sm, cp, 3.05, cruise_event=command_receipt(runtime, adopted.command.slcCruiseCommand, 3.05, 60., baseline))
|
||||
for event_id, (step, distance) in enumerate(zip((direction, direction, -direction, -direction), (1, 2, 1, 0), strict=True), 2):
|
||||
previous = float(helper.v_cruise_kph)
|
||||
helper.adjust_v_cruise(car.CarState.ButtonEvent.Type.accelCruise if step > 0 else
|
||||
car.CarState.ButtonEvent.Type.decelCruise, sm['carState'], metric)
|
||||
chosen = float(helper.v_cruise_kph)
|
||||
assert round(chosen * conversion) == display + direction * distance
|
||||
sm['carState'].vCruise = sm['carState'].vCruiseCluster = chosen
|
||||
seconds = 3.05 + event_id * .05
|
||||
stamp = START + round(seconds * 1e9)
|
||||
output = tick(runtime, sm, cp, seconds, cruise_event=CruiseEvent(event_id, stamp, previous / 3.6,
|
||||
chosen / 3.6, 'accel' if chosen > previous else 'decel', 'card-producer', stamp))
|
||||
assert output.result.override.event_receipt.consumed
|
||||
assert output.message.slcState.driverOverrideActive == (chosen != 72.)
|
||||
assert output.message.slcState.driverOverrideActive == (distance != 0)
|
||||
assert output.command is None
|
||||
assert helper.v_cruise_kph == pytest.approx(baseline, abs=1e-6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fallback', [0, 2])
|
||||
@@ -289,6 +325,53 @@ def test_pcm_software_set_speed_survives_native_speed_and_consumes_confirmation(
|
||||
assert helper.v_cruise_kph == pytest.approx(59.)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pending', [False, True])
|
||||
@pytest.mark.parametrize('signal,direction,normal_speed', [
|
||||
('CcAslButtnSetIncPress', 'accel', 61.), ('CcAslButtnSetDecPress', 'decel', 59.),
|
||||
])
|
||||
def test_ford_parser_buttons_adjust_set_speed_or_confirm_without_a_second_step(pending, signal, direction, normal_speed):
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, gen_empty_fingerprint
|
||||
from opendbc.car.ford.carstate import CarState
|
||||
from opendbc.car.ford.interface import CarInterface
|
||||
from opendbc.car.ford.values import CAR, DBC
|
||||
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS, SlcPendingConfirmation
|
||||
|
||||
cp = CarInterface.get_params(CAR.FORD_ESCAPE_MK4, gen_empty_fingerprint(), [], True, False, False)
|
||||
source = CarState(cp)
|
||||
parsers = source.get_can_parsers(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
helper = VCruiseHelper(cp)
|
||||
choice = SlcPendingConfirmation('drive', 1, 1) if pending else None
|
||||
now_ns = START
|
||||
|
||||
def frame(pressed, decision=None):
|
||||
nonlocal now_ns
|
||||
parsers[Bus.pt].update([(now_ns, [
|
||||
packer.make_can_msg('Steering_Data_FD1', parsers[Bus.pt].bus, {signal: pressed}),
|
||||
packer.make_can_msg('EngBrakeData', parsers[Bus.pt].bus, {'CcStat_D_Actl': 4, 'Veh_V_DsplyCcSet': 60}),
|
||||
])])
|
||||
now_ns += 10_000_000
|
||||
helper.update_v_cruise(source.update(parsers), True, True, decision, slc_enabled=True)
|
||||
|
||||
for _ in range(3):
|
||||
frame(0)
|
||||
helper.apply_slc_target(60. / 3.6, True)
|
||||
frame(1, choice)
|
||||
consumed = helper.slc_consumed_button
|
||||
assert (consumed is not None) == pending
|
||||
if pending:
|
||||
assert consumed.pending == choice and consumed.button == direction
|
||||
if direction == 'accel':
|
||||
helper.apply_slc_target(25., True)
|
||||
for _ in range(CRUISE_LONG_PRESS + 1):
|
||||
frame(1)
|
||||
assert helper.slc_consumed_button is None and helper.slc_cruise_change is None
|
||||
frame(0)
|
||||
assert helper.v_cruise_kph == (90. if pending and direction == 'accel' else 60. if pending else normal_speed)
|
||||
assert (helper.slc_cruise_change is None) == pending
|
||||
|
||||
|
||||
def test_automatic_countdown_never_consumes_plus_or_minus_and_late_accept_is_declined():
|
||||
candidate = acc.Candidate('dashboard', acc.ObservationIdentity(acc.IdentityKind.SESSION_VALUE, session_id='drive'), 20.)
|
||||
observation = acc.Observation(acc.ObservationKind.VALID, candidate)
|
||||
|
||||
@@ -14,7 +14,7 @@ from openpilot.starpilot.ui.large_speed_geometry import (WIDTH, CARD_HEIGHT, ACT
|
||||
|
||||
PARAM_KEY = "OnroadCustomizations"
|
||||
MAX_BYTES = 16384
|
||||
# Text alignment inside the cruise and speed-limit card; left is the original look.
|
||||
# Legacy saved layouts retain this field even though the paired speed widget uses fixed alignment.
|
||||
SPEED_ALIGNMENTS = ("left", "center", "right")
|
||||
PALETTE = {"cardFill": "#000000A6", "cardBorder": "#C4CDD0B4", "text": "#FFFFFFFF"}
|
||||
ROAD_COLORS = {"path": "#30FF9CFF", "pathEdge": "#00FF40FF", "laneLines": "#FFFFFFFF"}
|
||||
|
||||
Reference in New Issue
Block a user