IQ.Pilot Release Commit @ 45bb3db

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-15 13:35:06 -05:00
parent 7dc7dfd6f5
commit ccec601b0a
27 changed files with 262 additions and 177 deletions
+24 -24
View File
@@ -51,8 +51,8 @@
},
"python/_iqclosure/iqpilot/common/filter_simple.py": {
"mode": 420,
"sha256": "881243e472986ff0ba65c4044fa192ab5dd5e22c1d9fac3067ef953262a5fe3d",
"size": 961
"sha256": "4b34753a3d3eea6d1578d88d21350045173a3809d38aed232875d1c5f34b4141",
"size": 1929
},
"python/_iqclosure/iqpilot/common/gpio.py": {
"mode": 420,
@@ -141,8 +141,8 @@
},
"python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py": {
"mode": 420,
"sha256": "91008a050069861404b3f20977f5b8b6fdab4b3919e76e1ae95be49ce92f4ad8",
"size": 12298
"sha256": "0db179f0df2dd8949493bc49a5cdecb18d11383a20239c759ece59fa9ae874cc",
"size": 11398
},
"python/_iqclosure/iqpilot/selfdrive/controls/lib/drive_helpers.py": {
"mode": 420,
@@ -521,8 +521,8 @@
},
"python/_iqclosure/iqpilot/system/hardware/tici/hardware.py": {
"mode": 420,
"sha256": "f1ddffa9bdb5071a49fd65eb4b49958b60b1873e1ec70e6f3f2e2b2501abce2f",
"size": 25584
"sha256": "e8eae4e8a75e1751525471cf54a8555b7bfd7f08ad84bf2e75642b5745f222db",
"size": 25601
},
"python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py": {
"mode": 420,
@@ -576,37 +576,37 @@
},
"python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "0ecdfb14d5d39ea7481d41df103330bce083439123ae1abc8a80c15201e51ae0",
"size": 333808
"sha256": "bc5e1c99a6efca66a1f7d69ade1bd228870f965117deae86378c32e924432d39",
"size": 333944
},
"python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "6e9d753625942ba2e061d62dec251cbe838ba04bf047cd0db5ee56df0bbb3812",
"sha256": "5aa52680f9dd3ad393fabbed20e434168becf285ab5991df35dd8b3758d78419",
"size": 68072
},
"python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "b7e359549fea1ac8cb7e1d889b64c03cfa0d4cb6f2b0d34e0247bb7a57da7ec0",
"size": 267928
"sha256": "0a1048e38176dce88844f20f927be2dbca151e87e05e88e707bb807ff44595cc",
"size": 333544
},
"python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "445552ce49ea320ce13cacbaac3d73b6d38f5264bbd62af5be9ea47208d720f9",
"size": 404256
"sha256": "3bbcf0e311d1fed7a8b7f8e0b995875dc1f70f89a57681fe9a507721ba61452e",
"size": 404536
},
"python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "948fb552c0a0284979ae5e3fc9137d558ad7174158ffea232ae7d01466a61146",
"size": 464728
"sha256": "23c9a32a75daab86714e127902cef5c456703838db6e178a502aff3fe6cc50f2",
"size": 531776
},
"python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "9af9e13309f6e9907804b7d40182720510ce3fe512aea0ef4b3ebffb842bfe69",
"sha256": "cb4888d0a6d31851e15bbd7d53666d01382252cf5b46f994b4eeff2960ac502c",
"size": 473440
},
"python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "55252943e98e971c16cce659bb0038738383b34f87d933430626bbe2ca378b96",
"sha256": "bb23a72222a72983be8cacbf13972e36c966f8c4e3d844fcf572bb54501ef8be",
"size": 205848
},
"runtime": {
@@ -618,12 +618,12 @@
}
},
"signatures": {
"python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": "tmAlUdhZoUAWgQcRNuju1rSitT8oFbV6XAacY52kc2ffe3hVJQ6TgQO11YwSfKrzk9M1i93FEjCNm/y15j1lCw==",
"python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": "kxlAJMSafbk3c4o/XTqxp0nvB0BgP6+StWVpzSkZLLMuqsaHTYilBUYNUPWW2/4fDVgmFsqNDt8JMiOCYXPCCw==",
"python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": "MZG4X/QnCy/COsuMHPJw/b0oDFgMXhGI8E3SIKxpX4c+n02bxRwFxaRYS8hkm02S8ynTzZB5mUNhW/st7FkSAA==",
"python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": "rVudE3ZHsquCb0gV33LcnzkGG6nZV3kXN78YbHaKqXOfdpWEKLYrUXId7qng3BUJk+Mfyq7FC/ZVWIR9vvJSCw==",
"python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": "pFhB5A3P8QvOTueM/hu2H128OoT6S7zQdPhFz3HrFegRhuStWQ8tpWgu4pVPacfY+zEVeWdNDtWJrX5wKZ6UAQ==",
"python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so": "D58S28SrZ4SrTrGHAGDIHLpmV6ulSj0EQ4r1415/Fn8Eyb5ayknAdJtlTBiXihxRq1wc+aLBvjTbL7vepgaHCg==",
"python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so": "gzJJB6tzvLI9AL6ns6zvkTS0/xKEQRbn6uhE8INr1drMCqbim81OPMz5wesJaYNVlVTK72RT5tOy62mX0h1XDg=="
"python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so": "nb58JcxhuvZb66ykqx6qtiYcxuMrT9RxYLVPrySBh2c/2lK4YAEPOW3NYXItamo3c77tp5X12OOcTenV+hRFDQ==",
"python/iqpilot_private/emac/ffs_holder.cpython-312-aarch64-linux-gnu.so": "SlLArJdgtL4q44jc1iF2qOswWn0JaYKPLHM3WX+/CzkW6s5LLjPJLIP2/X9pT/xd8yb5jrrmNlkh756J0BlwDQ==",
"python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so": "XuilHMLAoLU+Uythp7DLZyUSzh5cd+Kd/ppdar60CvfI2ddWzyxUtWzRAj89MXosOg8TZlfvdbR+izFSaQaWCQ==",
"python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so": "3kHG7y4co2JjaMyYx+Nc2gti9VXv1EkDqgPuijpjtrUXxo2/z23HzW5OFR2tS5HOdSsoPp0QKxqNSJFU2vo1CQ==",
"python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so": "MqqTLheWl49buYG1d/aJk6W23WTEpBc0+BgTUAZHRHlfbrvvJzEZxutjS5EUd36QjOcqujdXns50NVrvbaAmDQ==",
"python/iqpilot_private/emac/model_context.cpython-312-aarch64-linux-gnu.so": "JI2Fqs5/uhABjC1TA5nPytJQXCWQLDYlEljLTkak6Dc8chUf2WiwBhWpcwFWhZO+QKwxERJXFdsG1ZTuPlTlDQ==",
"python/iqpilot_private/emac/rh_images.cpython-312-aarch64-linux-gnu.so": "BkfWTqw5Ei9EMLGwDNlDabsD/3sIXFJNkk+s/VR3Tm7+EKdkzJs4Ws/dshTDvg5DZjuwKmOdeMYZFxEpHcqvAw=="
}
}
@@ -1 +1 @@
ugmbC4W3Qqvv6F1w1mY3z3bdZV6HsCvSX+HQ+3vjBh03AKRaz41BYjjXdFDdFz45F/sOIeZvmomkvym4GezNCg==
WG1x9JR+nNQKjx6iHX9H9cBQWxWpHVCMn3NHirf7IDe/MnGX/Xsdv1tHKunzhleUga6DF9uhIsxThBewBFNfAQ==
@@ -1,3 +1,8 @@
from collections import deque
import numpy as np
class FirstOrderFilter:
def __init__(self, x0, rc, dt, initialized=True):
self.x = x0
@@ -32,3 +37,35 @@ class BounceFilter(FirstOrderFilter):
self.velocity.x = 0.0
self.x += self.velocity.x
return self.x
class MyMovingAverage:
def __init__(self, window_size, value=None):
self.window_size = window_size
if value is not None:
self.values = deque([value] * window_size, maxlen=window_size)
self.sum = value * window_size
self.result = value
else:
self.values = deque(maxlen=window_size)
self.sum = 0
self.result = 0
def set(self, value):
self.values.clear()
self.values.append(value)
self.sum = value
self.result = value
return value
def set_all(self, value):
self.values = deque([value] * self.window_size, maxlen=self.window_size)
self.sum = value * self.window_size
self.result = value
return value
def process(self, value, median=False):
self.values.append(value)
self.sum = sum(self.values)
self.result = float(np.median(self.values)) if median else float(self.sum) / len(self.values)
return self.result
@@ -25,11 +25,9 @@ NavManeuverPhase = custom.IQNavState.ManeuverPhase
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
LANE_CHANGE_TIME_MAX = 10.0
TURN_DESIRE_STOP_HOLD_TIME = 3.4
TURN_DESIRE_STOP_GAP_TIME = 0.2
TURN_DESIRE_STOP_CYCLE_TIME = TURN_DESIRE_STOP_HOLD_TIME + TURN_DESIRE_STOP_GAP_TIME
TURN_DESIRE_CYCLE_SPEED_MAX = 5 * CV.MPH_TO_MS
TURN_DESIRE_COMMIT_YAW_RATE = 0.08
# the model desire history spans 5 s, so one rising edge per 5 s keeps exactly one turn pulse visible
TURN_DESIRE_PULSE_HOLD_FRAMES = round(5.0 / DT_MDL) - 1
TURN_DESIRE_PULSE_CYCLE_FRAMES = TURN_DESIRE_PULSE_HOLD_FRAMES + 1
_LANE_CHANGE_DESIRES = {
(LaneChangeDirection.none, LaneChangeState.off): log.Desire.none,
@@ -52,7 +50,7 @@ _TURN_DESIRES = {
TurnDirection.turnRight: log.Desire.turnRight,
}
_STOP_CYCLING_TURN_DESIRES = {
_PULSED_TURN_DESIRES = {
log.Desire.turnLeft,
log.Desire.turnRight,
}
@@ -111,10 +109,8 @@ class DesireHelper:
self.lateral_edge_block = LateralEdgeBlock.none
self.lane_turn_direction = TurnDirection.none
self.nav_turn_direction = TurnDirection.none
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
self.turn_desire_frames = 0
self.turn_desire_cycle_input = log.Desire.none
self.turn_desire_committed = False
@staticmethod
def get_lane_change_direction(carstate):
@@ -222,43 +218,22 @@ class DesireHelper:
else:
self.lane_change_timer += DT_MDL
def _clear_turn_desire_stop_cycle(self) -> None:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
def _clear_turn_desire_pulse(self) -> None:
self.turn_desire_frames = 0
self.turn_desire_cycle_input = log.Desire.none
self.turn_desire_committed = False
def _cycle_turn_desire_when_stopped(self, desired_output: log.Desire) -> log.Desire:
if desired_output not in _STOP_CYCLING_TURN_DESIRES:
self._clear_turn_desire_stop_cycle()
def _pulse_turn_desire(self, desired_output: log.Desire) -> log.Desire:
if desired_output not in _PULSED_TURN_DESIRES:
self._clear_turn_desire_pulse()
return desired_output
if desired_output != self.turn_desire_cycle_input:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
self.turn_desire_frames = 0
self.turn_desire_cycle_input = desired_output
self.turn_desire_committed = False
if abs(getattr(self._last_carstate, "yawRate", 0.0)) >= TURN_DESIRE_COMMIT_YAW_RATE:
self.turn_desire_committed = True
if self.turn_desire_committed:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
return desired_output
if self._last_carstate.vEgo > TURN_DESIRE_CYCLE_SPEED_MAX:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
return desired_output
if not self.turn_desire_stop_active:
self.turn_desire_stop_active = True
self.turn_desire_stop_timer = 0.0
cycle_phase = self.turn_desire_stop_timer % TURN_DESIRE_STOP_CYCLE_TIME
self.turn_desire_stop_timer += DT_MDL
if cycle_phase >= TURN_DESIRE_STOP_HOLD_TIME:
cycle_frame = self.turn_desire_frames % TURN_DESIRE_PULSE_CYCLE_FRAMES
self.turn_desire_frames += 1
if cycle_frame >= TURN_DESIRE_PULSE_HOLD_FRAMES:
return log.Desire.none
return desired_output
@@ -271,7 +246,7 @@ class DesireHelper:
else:
desired_output = _LANE_CHANGE_DESIRES[(self.lane_change_direction, self.lane_change_state)]
self.desire = self._cycle_turn_desire_when_stopped(desired_output)
self.desire = self._pulse_turn_desire(desired_output)
def update(self, carstate, lateral_active, lane_change_prob, nav_state=None, modeldata=None, radar_state=None):
self._last_carstate = carstate
@@ -557,7 +557,7 @@ class Tici(HardwareBase):
if self.get_device_type() in ("tici", "tizi"):
if initial_eps_apn:
subprocess.call(["mmcli", "-m", "any", f'--3gpp-set-initial-eps-bearer-settings=apn={initial_eps_apn}'])
else:
elif is_comma_profile:
subprocess.call(["mmcli", "-m", "any", '--3gpp-set-initial-eps-bearer-settings=apn='])
cmds += [
+37
View File
@@ -1,3 +1,8 @@
from collections import deque
import numpy as np
class FirstOrderFilter:
def __init__(self, x0, rc, dt, initialized=True):
self.x = x0
@@ -32,3 +37,35 @@ class BounceFilter(FirstOrderFilter):
self.velocity.x = 0.0
self.x += self.velocity.x
return self.x
class MyMovingAverage:
def __init__(self, window_size, value=None):
self.window_size = window_size
if value is not None:
self.values = deque([value] * window_size, maxlen=window_size)
self.sum = value * window_size
self.result = value
else:
self.values = deque(maxlen=window_size)
self.sum = 0
self.result = 0
def set(self, value):
self.values.clear()
self.values.append(value)
self.sum = value
self.result = value
return value
def set_all(self, value):
self.values = deque([value] * self.window_size, maxlen=self.window_size)
self.sum = value * self.window_size
self.result = value
return value
def process(self, value, median=False):
self.values.append(value)
self.sum = sum(self.values)
self.result = float(np.median(self.values)) if median else float(self.sum) / len(self.values)
return self.result
+15 -40
View File
@@ -25,11 +25,9 @@ NavManeuverPhase = custom.IQNavState.ManeuverPhase
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
LANE_CHANGE_TIME_MAX = 10.0
TURN_DESIRE_STOP_HOLD_TIME = 3.4
TURN_DESIRE_STOP_GAP_TIME = 0.2
TURN_DESIRE_STOP_CYCLE_TIME = TURN_DESIRE_STOP_HOLD_TIME + TURN_DESIRE_STOP_GAP_TIME
TURN_DESIRE_CYCLE_SPEED_MAX = 5 * CV.MPH_TO_MS
TURN_DESIRE_COMMIT_YAW_RATE = 0.08
# the model desire history spans 5 s, so one rising edge per 5 s keeps exactly one turn pulse visible
TURN_DESIRE_PULSE_HOLD_FRAMES = round(5.0 / DT_MDL) - 1
TURN_DESIRE_PULSE_CYCLE_FRAMES = TURN_DESIRE_PULSE_HOLD_FRAMES + 1
_LANE_CHANGE_DESIRES = {
(LaneChangeDirection.none, LaneChangeState.off): log.Desire.none,
@@ -52,7 +50,7 @@ _TURN_DESIRES = {
TurnDirection.turnRight: log.Desire.turnRight,
}
_STOP_CYCLING_TURN_DESIRES = {
_PULSED_TURN_DESIRES = {
log.Desire.turnLeft,
log.Desire.turnRight,
}
@@ -111,10 +109,8 @@ class DesireHelper:
self.lateral_edge_block = LateralEdgeBlock.none
self.lane_turn_direction = TurnDirection.none
self.nav_turn_direction = TurnDirection.none
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
self.turn_desire_frames = 0
self.turn_desire_cycle_input = log.Desire.none
self.turn_desire_committed = False
@staticmethod
def get_lane_change_direction(carstate):
@@ -222,43 +218,22 @@ class DesireHelper:
else:
self.lane_change_timer += DT_MDL
def _clear_turn_desire_stop_cycle(self) -> None:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
def _clear_turn_desire_pulse(self) -> None:
self.turn_desire_frames = 0
self.turn_desire_cycle_input = log.Desire.none
self.turn_desire_committed = False
def _cycle_turn_desire_when_stopped(self, desired_output: log.Desire) -> log.Desire:
if desired_output not in _STOP_CYCLING_TURN_DESIRES:
self._clear_turn_desire_stop_cycle()
def _pulse_turn_desire(self, desired_output: log.Desire) -> log.Desire:
if desired_output not in _PULSED_TURN_DESIRES:
self._clear_turn_desire_pulse()
return desired_output
if desired_output != self.turn_desire_cycle_input:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
self.turn_desire_frames = 0
self.turn_desire_cycle_input = desired_output
self.turn_desire_committed = False
if abs(getattr(self._last_carstate, "yawRate", 0.0)) >= TURN_DESIRE_COMMIT_YAW_RATE:
self.turn_desire_committed = True
if self.turn_desire_committed:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
return desired_output
if self._last_carstate.vEgo > TURN_DESIRE_CYCLE_SPEED_MAX:
self.turn_desire_stop_timer = 0.0
self.turn_desire_stop_active = False
return desired_output
if not self.turn_desire_stop_active:
self.turn_desire_stop_active = True
self.turn_desire_stop_timer = 0.0
cycle_phase = self.turn_desire_stop_timer % TURN_DESIRE_STOP_CYCLE_TIME
self.turn_desire_stop_timer += DT_MDL
if cycle_phase >= TURN_DESIRE_STOP_HOLD_TIME:
cycle_frame = self.turn_desire_frames % TURN_DESIRE_PULSE_CYCLE_FRAMES
self.turn_desire_frames += 1
if cycle_frame >= TURN_DESIRE_PULSE_HOLD_FRAMES:
return log.Desire.none
return desired_output
@@ -271,7 +246,7 @@ class DesireHelper:
else:
desired_output = _LANE_CHANGE_DESIRES[(self.lane_change_direction, self.lane_change_state)]
self.desire = self._cycle_turn_desire_when_stopped(desired_output)
self.desire = self._pulse_turn_desire(desired_output)
def update(self, carstate, lateral_active, lane_change_prob, nav_state=None, modeldata=None, radar_state=None):
self._last_carstate = carstate
@@ -1,26 +1,25 @@
from types import SimpleNamespace
import numpy as np
import pytest
from iqpilot.cereal import custom, log
from iqpilot.common.realtime import DT_MDL
from iqpilot.selfdrive.controls.lib.desire_helper import (
DesireHelper,
TURN_DESIRE_STOP_CYCLE_TIME,
TURN_DESIRE_STOP_HOLD_TIME,
TURN_DESIRE_PULSE_CYCLE_FRAMES,
TURN_DESIRE_PULSE_HOLD_FRAMES,
)
TurnDirection = custom.IQTurnSignalDirection
MODEL_HISTORY_FRAMES = 100
def helper(v_ego=0.0, yaw_rate=0.0):
result = DesireHelper.__new__(DesireHelper)
result._last_carstate = SimpleNamespace(vEgo=v_ego, yawRate=yaw_rate)
result.turn_desire_stop_timer = 0.0
result.turn_desire_stop_active = False
result.turn_desire_frames = 0
result.turn_desire_cycle_input = log.Desire.none
result.turn_desire_committed = False
result.nav_turn_direction = TurnDirection.none
result.lane_turn_direction = TurnDirection.none
result.lane_change_direction = log.LaneChangeDirection.none
@@ -29,50 +28,48 @@ def helper(v_ego=0.0, yaw_rate=0.0):
return result
def visible_pulses(outputs):
previous = log.Desire.none
rising = []
for output in outputs:
rising.append(output != log.Desire.none and previous != output)
previous = output
rising = np.array(rising)
return np.array([rising[max(0, i - MODEL_HISTORY_FRAMES + 1):i + 1].sum() for i in range(len(rising))])
@pytest.mark.parametrize("source", ["manual", "nav"])
def test_manual_and_nav_turn_desires_receive_rising_edges(source):
h = helper()
@pytest.mark.parametrize("v_ego,yaw_rate", [(0.0, 0.0), (6.0, 0.0), (4.0, 0.3)])
def test_turn_desire_keeps_exactly_one_pulse_in_model_history(source, v_ego, yaw_rate):
h = helper(v_ego, yaw_rate)
if source == "manual":
h.lane_turn_direction = TurnDirection.turnLeft
else:
h.nav_turn_direction = TurnDirection.turnLeft
outputs = []
for _ in range(round((TURN_DESIRE_STOP_CYCLE_TIME + 2 * DT_MDL) / DT_MDL)):
for _ in range(4 * TURN_DESIRE_PULSE_CYCLE_FRAMES):
h._pick_desire_output()
outputs.append(h.desire)
gap_index = next(i for i, output in enumerate(outputs) if output == log.Desire.none)
assert gap_index * DT_MDL == pytest.approx(TURN_DESIRE_STOP_HOLD_TIME, abs=DT_MDL * 1.1)
assert log.Desire.turnLeft in outputs[gap_index + 1:]
gaps = [i for i, output in enumerate(outputs) if output == log.Desire.none]
assert gaps[0] == TURN_DESIRE_PULSE_HOLD_FRAMES
assert np.diff(gaps).tolist() == [TURN_DESIRE_PULSE_CYCLE_FRAMES] * (len(gaps) - 1)
assert set(visible_pulses(outputs)[MODEL_HISTORY_FRAMES:].tolist()) == {1}
def test_creeping_restarts_stopped_turn_cycle():
def test_new_turn_direction_restarts_pulse_immediately():
h = helper()
for _ in range(round(TURN_DESIRE_STOP_HOLD_TIME / DT_MDL)):
h._cycle_turn_desire_when_stopped(log.Desire.turnRight)
h._last_carstate.vEgo = 3.0
assert h._cycle_turn_desire_when_stopped(log.Desire.turnRight) == log.Desire.turnRight
h._last_carstate.vEgo = 0.0
assert h._cycle_turn_desire_when_stopped(log.Desire.turnRight) == log.Desire.turnRight
assert h.turn_desire_stop_timer == pytest.approx(DT_MDL)
def test_measured_turn_commitment_stops_cycling():
h = helper()
h._last_carstate.yawRate = -0.1
assert h._cycle_turn_desire_when_stopped(log.Desire.turnLeft) == log.Desire.turnLeft
h._last_carstate.yawRate = 0.0
outputs = [h._cycle_turn_desire_when_stopped(log.Desire.turnLeft) for _ in range(300)]
assert set(outputs) == {log.Desire.turnLeft}
def test_new_turn_direction_rearms_cycle_after_commitment():
h = helper(yaw_rate=0.1)
h._cycle_turn_desire_when_stopped(log.Desire.turnLeft)
h._last_carstate.yawRate = 0.0
h._cycle_turn_desire_when_stopped(log.Desire.turnRight)
assert h.turn_desire_committed is False
for _ in range(TURN_DESIRE_PULSE_HOLD_FRAMES // 2):
assert h._pulse_turn_desire(log.Desire.turnLeft) == log.Desire.turnLeft
assert h._pulse_turn_desire(log.Desire.turnRight) == log.Desire.turnRight
assert h.turn_desire_cycle_input == log.Desire.turnRight
assert h.turn_desire_frames == 1
def test_lane_change_desires_pass_through_and_reset_pulse():
h = helper()
h._pulse_turn_desire(log.Desire.turnLeft)
assert h._pulse_turn_desire(log.Desire.laneChangeLeft) == log.Desire.laneChangeLeft
assert h.turn_desire_frames == 0
assert h.turn_desire_cycle_input == log.Desire.none
+1
View File
@@ -226,6 +226,7 @@ class UIState(IQUIState):
"carOutput",
"carControl",
"vehicleParameters",
"egpuDockState",
] + self.sm_services_ext
)
+4 -4
View File
@@ -78,14 +78,14 @@
},
{
"name": "system",
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e.img.xz",
"hash": "aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e",
"hash_raw": "aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e",
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388.img.xz",
"hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388",
"hash_raw": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388",
"size": 6291456000,
"sparse": false,
"full_check": false,
"has_ab": true,
"ondevice_hash": "aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e",
"ondevice_hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388",
"url_parts": 10
}
]
+1 -1
View File
@@ -1 +1 @@
GOp80wlluTRkHZnMViznUD2H1iF6Y+VrWC8JsH2pVi2VeyZTTEPhdNMP7kH4EXYT7Syfa4mAgtdPLl9jXa4wAQ==
R39gJsoN/ESDuoPJTEUrOF8FXPwyf+dzf0s4pvpx53vIsXlY89OQjyooQQHMWsK4GgU9Dvv7trTuiXo3NP0XCQ==
+42 -13
View File
@@ -36,6 +36,9 @@ IQPILOT_MANIFEST_PUBLIC_KEY = bytes.fromhex("40ae3f81b77506ecc4982a1ca37ba1d6f87
AGNOS_MANIFEST_FILE = "system/hardware/tici/agnos.json"
LFS_POINTER_MAGIC = b"version https://git-lfs"
# Cloudflare cuts a proxied response off after ~100 s, so a part slower than ~900 KB/s only completes through range resumes.
PART_RESUME_LIMIT = 50
PART_RESUME_DELAY = 2
def _image_auth_module():
try:
@@ -73,12 +76,28 @@ def _download_headers(url: str) -> dict:
pass
return {}
def _open_image_response(url: str) -> requests.Response:
class RangeNotSupported(requests.exceptions.RequestException):
pass
def _ranged_get(url: str, headers: dict, offset: int) -> requests.Response:
if offset:
headers = {**headers, 'Range': f"bytes={offset}-"}
req = requests.get(url, stream=True, headers=headers, timeout=60)
req.raise_for_status()
if offset and req.status_code != 206:
req.close()
raise RangeNotSupported(f"server ignored range resume at {offset} for {url}")
return req
def _open_image_response(url: str, offset: int = 0) -> requests.Response:
auth = _download_headers(url)
req = requests.get(url, stream=True, headers={'Accept-Encoding': None, **auth}, timeout=60)
req.raise_for_status()
if int(req.headers.get('content-length') or 0) >= 1024:
return req
if not offset:
return req
req.close()
return _ranged_get(url, {'Accept-Encoding': None, **auth}, offset)
body = req.content
if not body.startswith(LFS_POINTER_MAGIC):
@@ -88,10 +107,10 @@ def _open_image_response(url: str) -> requests.Response:
size = int(meta["size"])
lfs_base = url.split("/raw/", 1)[0] + ".git/info/lfs"
req = requests.get(f"{lfs_base}/objects/{oid}", stream=True,
headers={'Accept-Encoding': None, 'Accept': 'application/vnd.git-lfs', **auth}, timeout=60)
if req.status_code == 200:
return req
try:
return _ranged_get(f"{lfs_base}/objects/{oid}", {'Accept-Encoding': None, 'Accept': 'application/vnd.git-lfs', **auth}, offset)
except (requests.exceptions.HTTPError, RangeNotSupported):
pass
batch = requests.post(f"{lfs_base}/objects/batch",
data=json.dumps({"operation": "download", "transfers": ["basic"],
@@ -101,10 +120,7 @@ def _open_image_response(url: str) -> requests.Response:
timeout=60)
batch.raise_for_status()
action = batch.json()["objects"][0]["actions"]["download"]
req = requests.get(action["href"], stream=True,
headers={'Accept-Encoding': None, **action.get("header", {})}, timeout=60)
req.raise_for_status()
return req
return _ranged_get(action["href"], {'Accept-Encoding': None, **action.get("header", {})}, offset)
def verify_manifest_signature(manifest_path: str) -> None:
@@ -131,8 +147,21 @@ class _ChainedParts:
def iter_content(self, chunk_size: int) -> Generator[bytes, None, None]:
for u in self.urls:
self.req = _open_image_response(u)
yield from self.req.iter_content(chunk_size=chunk_size)
received = 0
resumes = 0
while True:
try:
self.req = _open_image_response(u, received)
for chunk in self.req.iter_content(chunk_size=chunk_size):
received += len(chunk)
yield chunk
break
except (requests.exceptions.ChunkedEncodingError, requests.exceptions.ConnectionError):
resumes += 1
if resumes > PART_RESUME_LIMIT:
raise
print(f"Resuming {u} at {received} bytes ({resumes})", flush=True)
time.sleep(PART_RESUME_DELAY)
class StreamingDecompressor:
def __init__(self, url: str, parts: int = 0) -> None:
@@ -141,7 +170,7 @@ class StreamingDecompressor:
if parts > 1:
self.req = _ChainedParts([f"{url}.p{i:02d}" for i in range(parts)])
else:
self.req = _open_image_response(url)
self.req = _ChainedParts([url])
self.it = self.req.iter_content(chunk_size=1024 * 1024)
self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO)
self.eof = False
@@ -67,14 +67,14 @@
},
{
"name": "system",
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e.img.xz",
"hash": "aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e",
"hash_raw": "aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e",
"url": "https://git.konn3kt.com/IQ.Lvbs/iqos/raw/branch/master/system-1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388.img.xz",
"hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388",
"hash_raw": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388",
"size": 6291456000,
"sparse": false,
"full_check": false,
"has_ab": true,
"ondevice_hash": "aad7a8ca1a0ec5669d46e0d5c2794214b8089361be65ecb21d7feb4d4df00c5e",
"ondevice_hash": "1a3fd2a6c5d5530b6b5dd5d25311326c8b44fba22533dcfaafb1cce35485e388",
"url_parts": 10
}
]
@@ -1 +1 @@
+ZDnJpayd7Z4kRKRJ0LKCKKA0lzO998S/3IQFaJdozGGozP31GstEC0i0xYcaixG51ye2r8fB/GCDHGrK0kyAg==
Q7FSsS7kmbYAPqYe5W+Ht6xPNb9ekBQQlVYTBT/VNgR96+aazk2TOV/8909sS3n30bfvm/YUZc1MawSzoWGgAA==
+1 -1
View File
@@ -557,7 +557,7 @@ class Tici(HardwareBase):
if self.get_device_type() in ("tici", "tizi"):
if initial_eps_apn:
subprocess.call(["mmcli", "-m", "any", f'--3gpp-set-initial-eps-bearer-settings=apn={initial_eps_apn}'])
else:
elif is_comma_profile:
subprocess.call(["mmcli", "-m", "any", '--3gpp-set-initial-eps-bearer-settings=apn='])
cmds += [
+8 -1
View File
@@ -16,14 +16,21 @@ from iqpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS
W = TypeVar('W', bound='Widget')
_device = None
_device_unavailable = False
def device_awake() -> bool:
global _device
global _device, _device_unavailable
if _device is None:
if _device_unavailable:
return True
try:
from iqpilot.selfdrive.ui.ui_state import device
_device = device
# The setup zipapp has no msgq, and a failed import is never cached, so retrying costs ~70 ms per widget per frame.
except ModuleNotFoundError:
_device_unavailable = True
return True
except Exception:
return True
return _device.awake
+3 -1
View File
@@ -43,9 +43,11 @@ def _egpu_state(params: Params, engaged: bool) -> SourceState:
def resolve_source(params: Params, engaged: bool) -> tuple[str, SourceState]:
if params.get_bool("UsbGpuPresent") and not params.get_bool("IQEgpuDisabled"):
return "GPU", _egpu_state(params, engaged)
if params.get_bool("IQEmacEnabled"):
return "MAC", _emac_state(params, engaged)
if params.get_bool("UsbGpuPresent") or params.get_bool("IQEgpuEnabled"):
if params.get_bool("IQEgpuEnabled"):
return "GPU", _egpu_state(params, engaged)
return "", SourceState.HIDDEN
+20 -2
View File
@@ -15,10 +15,13 @@ from iqpilot.selfdrive.ui.ui_state import ui_state
from iqpilot.system.ui.lib.application import FontWeight, gui_app
from iqpilot.system.ui.lib.text_measure import measure_text_cached
from iqpilot.system.ui.widgets import Widget
from iqpilot.ui.onroad.theme import SECONDARY_TEXT
_POLL_S = 1.0
_FONT_SIZE = 70
_ICON_H = 76
_POWER_FONT_SIZE = 32
_POWER_GAP = 10
_ICONS = {"MAC": ("mac", 62 / 46), "GPU": ("egpu", 1.0)}
_GREY = rl.Color(165, 165, 170, 235)
_WHITE = rl.Color(255, 255, 255, 255)
@@ -75,6 +78,7 @@ class EmacStatusRenderer(Widget):
super().__init__()
self._params = Params()
self._font = gui_app.font(FontWeight.SEMI_BOLD)
self._power_font = gui_app.font(FontWeight.MEDIUM)
self._last_poll = 0.0
self._label = ""
self._state = SourceState.HIDDEN
@@ -95,5 +99,19 @@ class EmacStatusRenderer(Widget):
y = rect.y + rect.height / 2 - size.y / 2
draw_source_label(self._font, self._label, self._state, rl.Vector2(x, y), _FONT_SIZE)
return
draw_source_icon(self._label, self._state, _ICON_H,
rl.Vector2(rect.x + UI_BORDER_SIZE + BTN_SIZE // 2, rect.y + rect.height / 2))
center = rl.Vector2(rect.x + UI_BORDER_SIZE + BTN_SIZE // 2, rect.y + rect.height / 2)
draw_source_icon(self._label, self._state, _ICON_H, center)
if self._label == "GPU":
self._draw_power_draw(center)
def _draw_power_draw(self, icon_center: rl.Vector2) -> None:
sm = ui_state.sm
if not sm.alive["egpuDockState"] or sm.recv_frame["egpuDockState"] <= 0:
return
watts = sm["egpuDockState"].powerDrawW
if not math.isfinite(watts) or watts < 0:
return
text = f"{watts:.0f} W"
size = measure_text_cached(self._power_font, text, _POWER_FONT_SIZE)
pos = rl.Vector2(round(icon_center.x - size.x / 2), round(icon_center.y + _ICON_H / 2 + _POWER_GAP))
rl.draw_text_ex(self._power_font, text, pos, _POWER_FONT_SIZE, 0, SECONDARY_TEXT)
+10 -3
View File
@@ -33,9 +33,16 @@ def test_egpu_label_is_gpu():
assert label == "GPU"
def test_emac_wins_when_both_enabled():
label, _ = _resolve(IQEmacEnabled=True, IQEgpuEnabled=True,
MacModelReachable=True, UsbGpuPresent=True)
def test_present_dock_wins_over_emac():
assert _resolve(IQEmacEnabled=True, MacModelReachable=True,
UsbGpuPresent=True, UsbGpuActive=True) == ("GPU", SourceState.ACTIVE)
assert _resolve(IQEmacEnabled=True, UsbGpuPresent=True, UsbGpuActive=True) == ("GPU", SourceState.ACTIVE)
def test_emac_wins_without_consented_dock():
label, _ = _resolve(IQEmacEnabled=True, IQEgpuEnabled=True, MacModelReachable=True)
assert label == "MAC"
label, _ = _resolve(IQEmacEnabled=True, MacModelReachable=True, UsbGpuPresent=True, IQEgpuDisabled=True)
assert label == "MAC"
+1 -1
View File
@@ -23,7 +23,7 @@ if [ -z "$AGNOS_VERSION" ]; then
case "$DEVICE_MODEL" in
*)
export AGNOS_VERSION="IQ.OS 4.9.9"
export AGNOS_VERSION="IQ.OS 4.9.9.1"
;;
esac
fi