From 3ca76720fc16cec3291e5392f84312b5da3875d1 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:38:20 -0500 Subject: [PATCH] cruise speed and ui test --- common/watchdog.py | 14 +- panda/board/obj/body_h7.bin.signed | Bin 76440 -> 76440 bytes panda/board/obj/body_h7/bootstub.elf | Bin 112692 -> 112692 bytes panda/board/obj/body_h7/main.bin | Bin 76304 -> 76304 bytes panda/board/obj/body_h7/main.elf | Bin 367084 -> 367084 bytes panda/board/obj/bootstub.body_h7.bin | Bin 12644 -> 12644 bytes panda/board/obj/bootstub.panda.bin | Bin 11068 -> 11068 bytes panda/board/obj/bootstub.panda_h7.bin | Bin 18784 -> 18784 bytes panda/board/obj/bootstub.panda_h7_remote.bin | Bin 18784 -> 18784 bytes panda/board/obj/bootstub.panda_jungle_h7.bin | Bin 13956 -> 13956 bytes panda/board/obj/bootstub.panda_remote.bin | Bin 11068 -> 11068 bytes panda/board/obj/gitversion.h | 2 +- panda/board/obj/panda.bin.signed | Bin 68692 -> 68692 bytes panda/board/obj/panda/bootstub.elf | Bin 100760 -> 100760 bytes panda/board/obj/panda/main.bin | Bin 68556 -> 68556 bytes panda/board/obj/panda/main.elf | Bin 332748 -> 332748 bytes panda/board/obj/panda_h7.bin.signed | Bin 84372 -> 84372 bytes panda/board/obj/panda_h7/bootstub.elf | Bin 152728 -> 152728 bytes panda/board/obj/panda_h7/main.bin | Bin 84236 -> 84236 bytes panda/board/obj/panda_h7/main.elf | Bin 410420 -> 410420 bytes panda/board/obj/panda_h7_remote.bin.signed | Bin 84316 -> 84316 bytes panda/board/obj/panda_h7_remote/bootstub.elf | Bin 152736 -> 152736 bytes panda/board/obj/panda_h7_remote/main.bin | Bin 84180 -> 84180 bytes panda/board/obj/panda_h7_remote/main.elf | Bin 410384 -> 410384 bytes panda/board/obj/panda_jungle_h7.bin.signed | Bin 78280 -> 78280 bytes panda/board/obj/panda_jungle_h7/bootstub.elf | Bin 123936 -> 123936 bytes panda/board/obj/panda_jungle_h7/main.bin | Bin 78144 -> 78144 bytes panda/board/obj/panda_jungle_h7/main.elf | Bin 375148 -> 375148 bytes panda/board/obj/panda_remote.bin.signed | Bin 68648 -> 68648 bytes panda/board/obj/panda_remote/bootstub.elf | Bin 100768 -> 100768 bytes panda/board/obj/panda_remote/main.bin | Bin 68512 -> 68512 bytes panda/board/obj/panda_remote/main.elf | Bin 332796 -> 332796 bytes panda/board/obj/version | 2 +- selfdrive/car/cruise.py | 12 +- selfdrive/car/tests/test_cruise_speed.py | 44 ++++ .../settings/starpilot/longitudinal.py | 7 +- selfdrive/ui/stall_monitor.py | 131 ++++++++++++ selfdrive/ui/ui.cc | 200 +++++++++++++++++- selfdrive/ui/ui.py | 38 +++- system/ui/lib/application.py | 25 +++ 40 files changed, 445 insertions(+), 30 deletions(-) create mode 100644 selfdrive/ui/stall_monitor.py diff --git a/common/watchdog.py b/common/watchdog.py index ddb6f744e..a60dc1d73 100644 --- a/common/watchdog.py +++ b/common/watchdog.py @@ -2,21 +2,27 @@ import os import time import struct from openpilot.system.hardware.hw import Paths +from openpilot.common.swaglog import cloudlog WATCHDOG_FN = f"{Paths.shm_path()}/wd_" _LAST_KICK = 0.0 +_LAST_ERROR_LOG = 0.0 def kick_watchdog(): - global _LAST_KICK + global _LAST_KICK, _LAST_ERROR_LOG current_time = time.monotonic() if current_time - _LAST_KICK < 1.0: - return + return True try: with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f: f.write(struct.pack('= 5.0: + cloudlog.error(f"watchdog kick failed for pid {os.getpid()}: {e}") + _LAST_ERROR_LOG = current_time + return False diff --git a/panda/board/obj/body_h7.bin.signed b/panda/board/obj/body_h7.bin.signed index 16301e3b0eb99b29d196c7acf5fe36224329f72a..491ce0d25e3b8d71519e0396fe7e6e1adb9c949d 100644 GIT binary patch delta 152 zcmV;J0B8T0)dZN;1h5dQ2w`M7G&o^lIinb=fPY@K%OKC5WZ9x|wsff6qQ;VaPwm2- zf2PPF?_?gNj47*j+3;iH5YUi&Kk{otnt;EY66yzlikc(*_5nWGNMf2?0aM9l0U9eAtqBD|hv+QRxWTJZ<* Gy8(Otby9Z# delta 152 zcmV;J0B8T0)dZN;1h5dQ2xc*6Wnp15W1|?VfPZR>lFx4!RlM;BW7-&^m&w@Nk&R|m zPFoxCY*Z5oZ-GthdR2FBwl2?RtGNjlvQpb#LG!*(Y!>fg8v$h=MaOAlXZ=(W%Ki%> znB*>PL(p_bd-rZpYC#SUyGW$f5;xr0-^e;ptwwqrD^a#vL@__Qm<9h~mgZ6Eu%Mu) Gn*mIxdPWHV diff --git a/panda/board/obj/body_h7/bootstub.elf b/panda/board/obj/body_h7/bootstub.elf index 4d605163b3a98f419593ce2b1160062b6604b9dd..3495aa43fab4d47815408eb6de3bc8d7f984e48e 100755 GIT binary patch delta 27 jcmdn;fo;nNwhgW}9EmBGCKic_md(C4+kI^qpH~9_o-hic delta 27 jcmdn;fo;nNwhgW}9BGDWsfme3$<4kt+kI^qpH~9_qFM^} diff --git a/panda/board/obj/body_h7/main.bin b/panda/board/obj/body_h7/main.bin index 4b00877491dd45f611ffceb3eb876b1a38681646..06466228b96100089f4155ad8f9804ddb3b8c002 100755 GIT binary patch delta 23 fcmbPmg=NAOmJI@{I1*DVO)L@u_y7O^ delta 39 scmaE}S?tYbu?+&dIMNK$QWF!6lA9%VwM*<`1Y#y2X5KEbi{;9F08IW6OaK4? diff --git a/panda/board/obj/bootstub.body_h7.bin b/panda/board/obj/bootstub.body_h7.bin index a841786a162ebbce628ec62532b9c134dc9f1c19..42cc9a85c32a5703dbff078893dbb7dda913a1a2 100755 GIT binary patch delta 21 ccmaEo^dxD6s}4tEilvD~Vxr|{UmbB)0Aa)jV*mgE delta 21 ccmaEo^dxD6s}4t+VOnZpqEYf@UmbB)0A!vAw*UYD diff --git a/panda/board/obj/bootstub.panda.bin b/panda/board/obj/bootstub.panda.bin index 58c1808f2f985cb9387eb860bd2186794d98badc..8b8d3c20b92d2c9a07b0ae36761de533392404bd 100755 GIT binary patch delta 34 qcmdlJwkK?ZubNp(pJ3|A*Zo9oo}u>b(j7zp(pJ3|A*Zo9oo}u>b(qHVdc# diff --git a/panda/board/obj/bootstub.panda_h7.bin b/panda/board/obj/bootstub.panda_h7.bin index b73ac71e1b5068e9a2fa074c6611447b37a9a95e..f1be2c4efbe845f72ea56ae20117318c5237456e 100755 GIT binary patch delta 23 fcmaDbiSfZC#tog$9EmBGCKic_mYXL!pJxRCbs-5W delta 23 fcmaDbiSfZC#tog$9BGDWsfme3$(tuSpJxRCcl!x{ diff --git a/panda/board/obj/bootstub.panda_h7_remote.bin b/panda/board/obj/bootstub.panda_h7_remote.bin index b73ac71e1b5068e9a2fa074c6611447b37a9a95e..f1be2c4efbe845f72ea56ae20117318c5237456e 100755 GIT binary patch delta 23 fcmaDbiSfZC#tog$9EmBGCKic_mYXL!pJxRCbs-5W delta 23 fcmaDbiSfZC#tog$9BGDWsfme3$(tuSpJxRCcl!x{ diff --git a/panda/board/obj/bootstub.panda_jungle_h7.bin b/panda/board/obj/bootstub.panda_jungle_h7.bin index 7cdbfe8e6ce3d84ad45b3cadb83b8a7256fa3a9d..e5b4971e57c2d2b8fab7dc0ec7b5e403a4fa75fb 100755 GIT binary patch delta 21 ccmZq4ZOPrR*MK82#nQwgG0}4KQG@-g0AF4RSpWb4 delta 21 ccmZq4ZOPrR*MK9p(pJ3|A*Zo9oo}u>b(j7zp(pJ3|A*Zo9oo}u>b(qHVdc# diff --git a/panda/board/obj/gitversion.h b/panda/board/obj/gitversion.h index 409c55c92..4d9982d22 100644 --- a/panda/board/obj/gitversion.h +++ b/panda/board/obj/gitversion.h @@ -1,2 +1,2 @@ extern const uint8_t gitversion[19]; -const uint8_t gitversion[19] = "DEV-f1feaa2c-DEBUG"; +const uint8_t gitversion[19] = "DEV-ad948aa9-DEBUG"; diff --git a/panda/board/obj/panda.bin.signed b/panda/board/obj/panda.bin.signed index 60c4a4d6401f20601d25bc3c8e8abf578da25372..ce406e43ba162371d3bf7b128395c38b7dc25f94 100644 GIT binary patch delta 153 zcmV;K0A~Nxm;}_A1hDX$2w`M7G&o^lIkWhh+;D%3-_1J8o3NB(otNB^L)um2!QYl@ zIFxilaxH$&lIf4f`NM}Oxbgz;@I5^gb9C~_;5Lch=9$?`Vkl}BU_>0~6NH8_ru#ju zw4t;6snr!F8^};hsRkUJ0j7CvxD-(r8Xk<@>lS161|!CP=&-jxk?t_-KkUs)S^%*Z H9WViPE;vln delta 153 zcmV;K0A~Nxm;}_A1hDX$2xc*6Wnp15W3%|0+;D$^F;&Er9~wnTkSqP>HzDT7-#{-Y z^YaVjT{bB_fwUf_{0P$jwPIOHD;Wg_sW+}l7-s!cVDG}+%9*lx!_n}(6!}WFmqRRZ z8;N%fMJcijUAIQ<9BeqfdN>)~RZ|1wz^`YHM2<}CN@GvNC^%57Y}iDZlSK0y_jY2s H2gw28@&icC diff --git a/panda/board/obj/panda/bootstub.elf b/panda/board/obj/panda/bootstub.elf index 95af89862cbdd5b23c9832b3779b43f84ee59243..349a5123a8c17e48a7f82ab79b80f356b2698184 100755 GIT binary patch delta 40 wcmbO+nQg{owhg{!f{7`XCKic_mbxykPND7$4GfbfnkqEcnQgB#W0VR801kQ#sQ>@~ delta 40 wcmbO+nQg{owhg{!f@y|nsfme3$+|ACPND7$4GfbfnkqEcnQgB#W0VR802b&B{Qv*} diff --git a/panda/board/obj/panda/main.bin b/panda/board/obj/panda/main.bin index 58185363efcc95bbfa5f359729bf33d161ececd8..ed7299b7fdb14481b34521659431cef56fd31342 100755 GIT binary patch delta 23 fcmX>zo#o7QmJJ_faU`Z#nph+zT5kR^>q-Ftfu;)o delta 23 fcmX>zo#o7QmJJ_faikfhr6wjCC2#&Q>q-Ftgn$cE diff --git a/panda/board/obj/panda/main.elf b/panda/board/obj/panda/main.elf index ebdeb25742f7b5dbf1e40eb9205bbd7434eac6d4..62ba4f0d33556dfcc9a75db726da03e04a0baf80 100755 GIT binary patch delta 39 tcmX@JT;$AhkqsYKaU`Z#nph+zS~mY!)&64@BM>tIG4u8xt5~jN004z^6a@eP delta 39 tcmX@JT;$AhkqsYKaikfhr6wjCB{%tIG4u8xt5~jN0051R6j=ZO diff --git a/panda/board/obj/panda_h7.bin.signed b/panda/board/obj/panda_h7.bin.signed index cf14569c64ede204146c3a96c3a1e1100d5ac0f8..cc9665bbaca176793c2ab08e0445efd42bf83751 100644 GIT binary patch delta 152 zcmV;J0B8S{lLeHM1)#+y^1`Q_t>Pa|(hzI}8Clw7Dwl z{yWs*j?bwH+HJ+v_`_)zsPm2xE`pz$De?P01F05b3JaM4go?sGLkZ6;;`-vFTXhgM zptC~NIz|svCed7(LRm=j{g4Ox$Dk_#`ZRv@dPlTg3dfYh7>uvuEHG83umX delta 42 tcmdneCb^|ea=|r@G{dyi#6+XyW{zv^9M>3um;ZpP?jJEexgbcNmyrA9#)@79mHgE7p(W>_@Js# z8a&?fJ9Zb~*m5|S6 HPly4p<3~r~ diff --git a/panda/board/obj/panda_h7_remote/bootstub.elf b/panda/board/obj/panda_h7_remote/bootstub.elf index 9201f9c8c27b4d36e147811e628f666845d954fc..89c554dcd298943e2cb67ab5131092891aee5e0c 100755 GIT binary patch delta 31 ncmZ3ml5@dI&JCTx9EmBGCKic_mdz7`+b0GyZl4&;#Lox-!;1?Z delta 31 ncmZ3ml5@dI&JCTx9BGDWsfme3$;}gk+b0GyZl4&;#Lox-$ps5? diff --git a/panda/board/obj/panda_h7_remote/main.bin b/panda/board/obj/panda_h7_remote/main.bin index de8cb3036c8728ed154ce8ccacf0b57efefcf79b..2aaff35e8489b6f0db9f6c51b990710e13f3c516 100755 GIT binary patch delta 23 fcmcaIk@d<%)(t0)awMi$nph+zT5i5@l%*K}fQSlC delta 23 fcmcaIk@d<%)(t0)a-I$M9EmBGCKic_mdzKgwqLl)2*gZ4%nZaV+b>*Yb;tz(yI$M9BGDWsfme3$;}t8wqLl)2*gZ4%nZaV+b>*Yb;tz(#>W+1 diff --git a/panda/board/obj/panda_jungle_h7.bin.signed b/panda/board/obj/panda_jungle_h7.bin.signed index d815b9f3aa03d0efd1877df47de79253400bec3c..5dbea9d98d3dbfe4598e5f4adcc1afc1b28012aa 100644 GIT binary patch delta 153 zcmV;K0A~Nl;{?d#1hBQP2w`M7G&o^lIkUa4i-La#CKlss;CDW9(+Wv9oxi2Ak(ttW z?jyB(O4sF6a`;0uFx<(gBLXOHLpj&}s(+;y=9<7IuYB;F<&-{$A?;Z}^oLQNj{C)E zBWy*P`no1KJE3o9&Cm^I7If6eJ)=~T?rK}xjXmaq%Q$eW3Z|rA4#_#bh76{u0{241 HGAsdI{dh~& delta 153 zcmV;K0A~Nl;{?d#1hBQP2xc*6Wnp15W3#=ki-LblHNsI&2f>n6%JzgHJDMW8A-xa; z>>jv#40>)Ht- zwG3-y9wkdLyYZIC4qYzg!{+SBs`lpRJDB}<`~CIA!3#e;X#r?M%#+oxN{pm0k1F%R Hl>`B7MP5wL diff --git a/panda/board/obj/panda_jungle_h7/bootstub.elf b/panda/board/obj/panda_jungle_h7/bootstub.elf index 7ebbef8a3df6f554b5dad9b1d964b17ae7f9e7de..38bc47faa3cecc2b646faf31d15cff2a51b381f0 100755 GIT binary patch delta 27 jcmZ2*f_=dW_6>U-I1*DVO)L@U-IMNK$QWF!6lADh@Y(MJ27`+$(rR58d diff --git a/panda/board/obj/panda_jungle_h7/main.bin b/panda/board/obj/panda_jungle_h7/main.bin index ba8df3e604ab57b58e67f98c38800fd477cab9ce..30a17132d24c33d7cccebf2c7420953a8f03bb4d 100755 GIT binary patch delta 23 fcmX@`h~>Z|mJM6iaU`Z#nph+zT5jIEuDJ;Shv5qF delta 23 fcmX@`h~>Z|mJM6iaikfhr6wjCC2!uluDJ;Sin|L$ diff --git a/panda/board/obj/panda_jungle_h7/main.elf b/panda/board/obj/panda_jungle_h7/main.elf index 540bf0b9d24c3015a9c1e0dc2b246b92092fd604..41d11117a2342a3c856184528b878ea5f54f72bf 100755 GIT binary patch delta 39 tcmaF!N$kxhu?<`IaU`Z#nph+zS~l<9*S>cjBM>tIG4uAl`&ich0sxit6k-4X delta 39 tcmaF!N$kxhu?<`Iaikfhr6wjCB{%Qg*S>cjBM>tIG4uAl`&ich0sx*46t(~W diff --git a/panda/board/obj/panda_remote.bin.signed b/panda/board/obj/panda_remote.bin.signed index df480c4169cebb01a7bc6101726bb1f6afdb8659..7805d4573dd8d02589045056cb47929c381b5b0c 100644 GIT binary patch delta 153 zcmV;K0A~NFm;|Vp1hB-K2w`M7G&o^lIkU`~+;D$okAkc=SB4RuI$d%SAh?pkMOHq( zurFK3s1{+4zjeZU4uzzS%SUz=)a}O)Pgn+vg$g?5*1%>4%?!kzmE3sQ9W-lTc<^UI zcovMv%Ui)o+6d@xsf&%JBrQKWK?WS5ue0tbqA62P0w4t%F|_3q6VH_Wc){8!&IDsB HfwTcB_sK-V delta 153 zcmV;K0A~NFm;|Vp1hB-K2xc*6Wnp15W3$Yf+;D$|N~w{q-FtcG3z@ delta 23 fcmZ25on^svmJLT{aikfhr6wjCC2u}6>q-Ftd8`Vf diff --git a/panda/board/obj/panda_remote/main.elf b/panda/board/obj/panda_remote/main.elf index b756c885eb8047d6c71a8aab6c721eea91367dc0..308f84b988d6abc4554510f59d59e6986584150c 100755 GIT binary patch delta 39 scmeyfT;$Jkkqt*yaU`Z#nph+zS~j0q)qZ9bBM>tIG4u8_t5^&&0fD>{x&QzG delta 39 scmeyfT;$Jkkqt*yaikfhr6wjCB{!d0)qZ9bBM>tIG4u8_t5^&&0g2WV4gdfE diff --git a/panda/board/obj/version b/panda/board/obj/version index df1bd3e78..230301a37 100644 --- a/panda/board/obj/version +++ b/panda/board/obj/version @@ -1 +1 @@ -DEV-f1feaa2c-DEBUG \ No newline at end of file +DEV-ad948aa9-DEBUG \ No newline at end of file diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 589bd36f4..76b338d11 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -50,14 +50,6 @@ class VCruiseHelper: def _get_cruise_delta_interval(interval: float | None) -> float: return interval if isinstance(interval, (int, float)) and interval > 0 else 1.0 - def _normalize_initialized_v_cruise(self, v_cruise_kph: float, starpilot_toggles: SimpleNamespace) -> float: - cruise_increase = self._get_cruise_delta_interval(starpilot_toggles.cruise_increase) - if cruise_increase % 5 != 0: - return v_cruise_kph - - v_cruise_delta = self._get_short_press_delta(starpilot_toggles.is_metric, starpilot_toggles) - return round(round(v_cruise_kph / v_cruise_delta) * v_cruise_delta, 1) - @property def v_cruise_initialized(self): return self.v_cruise_kph != V_CRUISE_UNSET @@ -162,7 +154,9 @@ class VCruiseHelper: and self.v_cruise_initialized or (self.gm_cc_only and resume_prev_button)): self.v_cruise_kph = self.v_cruise_kph_last elif desired_speed_limit > 0 and getattr(starpilot_toggles, "set_speed_limit", False): - initialized_speed_limit_kph = self._normalize_initialized_v_cruise(desired_speed_limit * CV.MS_TO_KPH, starpilot_toggles) + # Respect the exact SLC limit+offset on engage instead of snapping upward to + # the custom cruise-button interval. + initialized_speed_limit_kph = round(desired_speed_limit * CV.MS_TO_KPH, 1) self.v_cruise_kph = float(np.clip(initialized_speed_limit_kph, V_CRUISE_MIN, V_CRUISE_MAX)) else: self.v_cruise_kph = int(round(np.clip(CS.vEgo * CV.MS_TO_KPH, engage_floor_kph, V_CRUISE_MAX))) diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index c6a3b78ba..ab138f34c 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -207,6 +207,22 @@ class TestVCruiseHelper: assert self.v_cruise_helper.v_cruise_kph == pytest.approx(55 * CV.MPH_TO_KPH) + def test_initialize_v_cruise_keeps_exact_speed_limit_offset(self): + self.reset_cruise_speed_state() + self.starpilot_toggles.set_speed_limit = True + self.starpilot_toggles.cruise_increase = 5 + + desired_speed_limit = 38 * CV.MPH_TO_MS + self.v_cruise_helper.initialize_v_cruise( + car.CarState(vEgo=70 * CV.MPH_TO_MS), + experimental_mode=False, + resume_prev_button=False, + starpilot_toggles=self.starpilot_toggles, + desired_speed_limit=desired_speed_limit, + ) + + assert self.v_cruise_helper.v_cruise_kph == pytest.approx(38 * CV.MPH_TO_KPH) + def test_speed_limit_confirmation_does_not_adjust_cruise(self): self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False) initial_v_cruise_kph = self.v_cruise_helper.v_cruise_kph @@ -260,3 +276,31 @@ class TestVCruiseHelper: ) assert self.v_cruise_helper.v_cruise_kph == pytest.approx(initial_v_cruise_kph + IMPERIAL_INCREMENT) + + def test_zero_custom_cruise_toggles_fall_back_to_single_step(self): + self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False) + initial_v_cruise_kph = self.v_cruise_helper.v_cruise_kph + self.starpilot_toggles.cruise_increase = 0 + self.starpilot_toggles.cruise_increase_long = 0 + + pressed_cs = car.CarState(cruiseState={"available": True}) + pressed_cs.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=True)] + self.v_cruise_helper.update_v_cruise( + pressed_cs, + enabled=True, + is_metric=False, + speed_limit_changed=False, + starpilot_toggles=self.starpilot_toggles, + ) + + released_cs = car.CarState(cruiseState={"available": True}) + released_cs.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=False)] + self.v_cruise_helper.update_v_cruise( + released_cs, + enabled=True, + is_metric=False, + speed_limit_changed=False, + starpilot_toggles=self.starpilot_toggles, + ) + + assert self.v_cruise_helper.v_cruise_kph == pytest.approx(initial_v_cruise_kph + IMPERIAL_INCREMENT) diff --git a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py index db4259d24..d2e492904 100644 --- a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py +++ b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py @@ -576,7 +576,7 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel): { "title": tr_noop("Cruise Interval"), "type": "value", - "get_value": lambda: f"{self._params.get_int('CustomCruise')} mph", + "get_value": lambda: f"{max(1, self._params.get_int('CustomCruise'))} mph", "on_click": lambda: self._show_speed_selector("CustomCruise"), "color": "#597497", "visible": lambda: self._params.get_bool("QOLLongitudinal"), @@ -584,7 +584,7 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel): { "title": tr_noop("Cruise Long"), "type": "value", - "get_value": lambda: f"{self._params.get_int('CustomCruiseLong')} mph", + "get_value": lambda: f"{max(1, self._params.get_int('CustomCruiseLong'))} mph", "on_click": lambda: self._show_speed_selector("CustomCruiseLong"), "color": "#597497", "visible": lambda: self._params.get_bool("QOLLongitudinal"), @@ -662,7 +662,8 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel): self._params.put_int(key, int(val)) self._rebuild_grid() - gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497")) + current = max(1, self._params.get_int(key)) + gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 1, 100, 1, current, on_close, unit=" mph", color="#597497")) def _show_int_selector(self, key, min_v, max_v, unit=""): def on_close(res, val): diff --git a/selfdrive/ui/stall_monitor.py b/selfdrive/ui/stall_monitor.py new file mode 100644 index 000000000..0e9ced8ca --- /dev/null +++ b/selfdrive/ui/stall_monitor.py @@ -0,0 +1,131 @@ +import os +import sys +import time +import traceback +import threading +from pathlib import Path + +from openpilot.common.swaglog import cloudlog + + +def _default_dump_dir() -> Path: + for candidate in ("/data/log", "/tmp"): + if os.path.isdir(candidate) and os.access(candidate, os.W_OK): + return Path(candidate) + return Path.cwd() + + +class UIStallMonitor: + def __init__(self, name: str): + self._name = name + self._threshold_s = float(os.getenv("UI_STALL_PROBE_MAX_DT", "5")) + self._poll_s = float(os.getenv("UI_STALL_PROBE_POLL_DT", "0.25")) + self._dump_dir = _default_dump_dir() + self._main_thread_id = threading.get_ident() + + now = time.monotonic() + self._last_progress = now + self._phase = "init" + self._phase_entered = now + self._stall_reported = False + self._stalled_since = now + self._stalled_phase = self._phase + + self._lock = threading.Lock() + self._stop_event = threading.Event() + self._thread = threading.Thread(target=self._run, name=f"{name}_stall_probe", daemon=True) + + def start(self) -> None: + if self._threshold_s <= 0.0: + return + self._thread.start() + + def stop(self) -> None: + if self._threshold_s <= 0.0: + return + self._stop_event.set() + self._thread.join(timeout=1.0) + + def progress(self, phase: str) -> None: + now = time.monotonic() + recovered = None + + with self._lock: + if phase != self._phase: + self._phase = phase + self._phase_entered = now + self._last_progress = now + + if self._stall_reported: + recovered = (now - self._stalled_since, self._stalled_phase, phase) + self._stall_reported = False + + if recovered is not None: + stalled_for_s, stalled_phase, current_phase = recovered + cloudlog.warning(f"{self._name} stall recovered after {stalled_for_s:.1f}s (stalled_phase={stalled_phase}, current_phase={current_phase})") + + def _run(self) -> None: + while not self._stop_event.wait(self._poll_s): + now = time.monotonic() + with self._lock: + stalled_for_s = now - self._last_progress + phase = self._phase + phase_for_s = now - self._phase_entered + already_reported = self._stall_reported + + if stalled_for_s < self._threshold_s or already_reported: + continue + + dump = self._build_dump(now, phase, stalled_for_s, phase_for_s) + dump_path = self._write_dump(dump) + with self._lock: + self._stall_reported = True + self._stalled_since = now + self._stalled_phase = phase + + preview = self._main_thread_preview() + path_s = str(dump_path) if dump_path is not None else "" + cloudlog.error(f"{self._name} main loop stalled for {stalled_for_s:.1f}s in phase={phase} (phase_for={phase_for_s:.1f}s) dump={path_s}\n{preview}") + + def _build_dump(self, now: float, phase: str, stalled_for_s: float, phase_for_s: float) -> str: + frames = sys._current_frames() + threads = {thread.ident: thread for thread in threading.enumerate()} + lines = [ + f"name={self._name}", + f"pid={os.getpid()}", + f"wall_time={time.strftime('%Y-%m-%dT%H:%M:%S%z')}", + f"monotonic={now:.6f}", + f"stalled_for_s={stalled_for_s:.3f}", + f"phase={phase}", + f"phase_for_s={phase_for_s:.3f}", + "", + ] + + ordered_idents = sorted(frames.keys(), key=lambda ident: ident != self._main_thread_id) + for ident in ordered_idents: + thread = threads.get(ident) + name = thread.name if thread is not None else "" + daemon = thread.daemon if thread is not None else None + lines.append(f"Thread {name} ident={ident} daemon={daemon}") + lines.extend(traceback.format_stack(frames[ident])) + lines.append("") + + return "".join(line if line.endswith("\n") else f"{line}\n" for line in lines) + + def _main_thread_preview(self) -> str: + frame = sys._current_frames().get(self._main_thread_id) + if frame is None: + return "main_thread_stack=" + stack_lines = traceback.format_stack(frame) + preview = "".join(stack_lines[-8:]).strip() + return f"main_thread_stack:\n{preview}" if preview else "main_thread_stack=" + + def _write_dump(self, dump: str) -> Path | None: + timestamp = time.strftime("%Y%m%d_%H%M%S") + path = self._dump_dir / f"{self._name}_stall_{os.getpid()}_{timestamp}.log" + try: + path.write_text(dump) + return path + except OSError as e: + cloudlog.error(f"failed to write {self._name} stall dump to {path}: {e}") + return None diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index d29752aa8..0b9601a08 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -1,7 +1,21 @@ #include "selfdrive/ui/ui.h" #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include @@ -14,6 +28,178 @@ #define BACKLIGHT_DT 0.05 #define BACKLIGHT_TS 10.00 +namespace { + +enum class UIStallPhase { + INIT = 0, + UPDATE_START, + AFTER_SOCKETS, + AFTER_STATE, + AFTER_STATUS, + AFTER_WATCHDOG, + AFTER_EMIT, + AFTER_FS_UPDATE, + IDLE, +}; + +std::atomic ui_stall_last_progress_ns{0}; +std::atomic ui_stall_phase{static_cast(UIStallPhase::INIT)}; +std::atomic ui_stall_frame{0}; +std::atomic ui_stall_reported{false}; +std::atomic ui_stall_reported_ns{0}; +std::atomic ui_stall_reported_phase{static_cast(UIStallPhase::INIT)}; +std::atomic ui_stall_dump_fd{-1}; +pthread_t ui_main_thread{}; + +double read_env_double(const char *name, double default_value) { + const char *value = std::getenv(name); + if (value == nullptr || *value == '\0') { + return default_value; + } + + char *end = nullptr; + double parsed = std::strtod(value, &end); + if (end == value || (end != nullptr && *end != '\0') || parsed <= 0.0) { + return default_value; + } + return parsed; +} + +const char *ui_stall_phase_name(UIStallPhase phase) { + switch (phase) { + case UIStallPhase::INIT: return "init"; + case UIStallPhase::UPDATE_START: return "update_start"; + case UIStallPhase::AFTER_SOCKETS: return "after_sockets"; + case UIStallPhase::AFTER_STATE: return "after_state"; + case UIStallPhase::AFTER_STATUS: return "after_status"; + case UIStallPhase::AFTER_WATCHDOG: return "after_watchdog"; + case UIStallPhase::AFTER_EMIT: return "after_emit"; + case UIStallPhase::AFTER_FS_UPDATE: return "after_fs_update"; + case UIStallPhase::IDLE: return "idle"; + } + return "unknown"; +} + +std::string ui_stall_dump_dir() { + return access("/data/log", W_OK) == 0 ? "/data/log" : "/tmp"; +} + +void ui_stall_signal_handler(int sig) { + const int fd = ui_stall_dump_fd.load(std::memory_order_relaxed); + if (fd < 0) { + return; + } + + char header[256]; + const pid_t tid = static_cast(syscall(SYS_gettid)); + const int header_len = std::snprintf(header, sizeof(header), + "=== UI stall backtrace (signal=%d pid=%d tid=%d) ===\n", + sig, getpid(), tid); + if (header_len > 0) { + write(fd, header, header_len); + } + + void *frames[128]; + const int frame_count = backtrace(frames, 128); + backtrace_symbols_fd(frames, frame_count, fd); + write(fd, "\n", 1); +} + +void ui_stall_progress(UIStallPhase phase, uint64_t frame = 0) { + const uint64_t now = nanos_since_boot(); + ui_stall_phase.store(static_cast(phase), std::memory_order_relaxed); + ui_stall_frame.store(frame, std::memory_order_relaxed); + ui_stall_last_progress_ns.store(now, std::memory_order_relaxed); + + if (ui_stall_reported.exchange(false, std::memory_order_relaxed)) { + const uint64_t stall_started = ui_stall_reported_ns.load(std::memory_order_relaxed); + const UIStallPhase stalled_phase = static_cast(ui_stall_reported_phase.load(std::memory_order_relaxed)); + const double stalled_for_s = stall_started == 0 ? 0.0 : (now - stall_started) / 1e9; + LOGW("UI stall recovered after %.1fs (stalled_phase=%s current_phase=%s frame=%llu)", + stalled_for_s, + ui_stall_phase_name(stalled_phase), + ui_stall_phase_name(phase), + static_cast(frame)); + } +} + +void start_ui_stall_monitor() { + static std::once_flag once; + std::call_once(once, [] { + ui_main_thread = pthread_self(); + std::signal(SIGUSR1, ui_stall_signal_handler); + ui_stall_progress(UIStallPhase::INIT, 0); + + const double stall_probe_dt = read_env_double("UI_STALL_PROBE_MAX_DT", 5.0); + if (stall_probe_dt <= 0.0) { + return; + } + + std::thread([stall_probe_dt]() { + using namespace std::chrono_literals; + constexpr auto poll_interval = 250ms; + + while (true) { + std::this_thread::sleep_for(poll_interval); + + const uint64_t now = nanos_since_boot(); + const uint64_t last_progress = ui_stall_last_progress_ns.load(std::memory_order_relaxed); + if (last_progress == 0) { + continue; + } + + const double stalled_for_s = (now - last_progress) / 1e9; + if (stalled_for_s < stall_probe_dt) { + continue; + } + + bool expected = false; + if (!ui_stall_reported.compare_exchange_strong(expected, true, std::memory_order_relaxed)) { + continue; + } + + const UIStallPhase phase = static_cast(ui_stall_phase.load(std::memory_order_relaxed)); + const uint64_t frame = ui_stall_frame.load(std::memory_order_relaxed); + ui_stall_reported_ns.store(now, std::memory_order_relaxed); + ui_stall_reported_phase.store(static_cast(phase), std::memory_order_relaxed); + + const std::string path = ui_stall_dump_dir() + "/qt_ui_stall_" + std::to_string(getpid()) + "_" + std::to_string(now) + ".log"; + int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0644); + if (fd >= 0) { + char header[256]; + const int header_len = std::snprintf(header, sizeof(header), + "phase=%s frame=%llu stalled_for_s=%.3f\n", + ui_stall_phase_name(phase), + static_cast(frame), + stalled_for_s); + if (header_len > 0) { + write(fd, header, header_len); + } + + ui_stall_dump_fd.store(fd, std::memory_order_relaxed); + pthread_kill(ui_main_thread, SIGUSR1); + std::this_thread::sleep_for(50ms); + ui_stall_dump_fd.store(-1, std::memory_order_relaxed); + close(fd); + LOGE("UI main thread stalled for %.1fs (phase=%s frame=%llu dump=%s)", + stalled_for_s, + ui_stall_phase_name(phase), + static_cast(frame), + path.c_str()); + } else { + LOGE("UI main thread stalled for %.1fs (phase=%s frame=%llu dump_open_failed errno=%d)", + stalled_for_s, + ui_stall_phase_name(phase), + static_cast(frame), + errno); + } + } + }).detach(); + }); +} + +} // namespace + static void update_sockets(UIState *s) { s->sm->update(0); } @@ -144,6 +330,7 @@ void UIState::updateStatus(StarPilotUIState *fs) { } UIState::UIState(QObject *parent) : QObject(parent) { + start_ui_stall_monitor(); sm = std::make_unique(std::vector{ "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", @@ -156,17 +343,26 @@ UIState::UIState(QObject *parent) : QObject(parent) { timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIState::update); timer->start(1000 / UI_FREQ); + ui_stall_progress(UIStallPhase::IDLE, sm->frame); } void UIState::update() { + ui_stall_progress(UIStallPhase::UPDATE_START, sm->frame); update_sockets(this); + ui_stall_progress(UIStallPhase::AFTER_SOCKETS, sm->frame); update_state(this, starpilotUIState()); + ui_stall_progress(UIStallPhase::AFTER_STATE, sm->frame); updateStatus(starpilotUIState()); + ui_stall_progress(UIStallPhase::AFTER_STATUS, sm->frame); if (sm->frame % UI_FREQ == 0) { - watchdog_kick(nanos_since_boot()); + if (!watchdog_kick(nanos_since_boot())) { + LOGE("UI watchdog kick failed at frame %llu", static_cast(sm->frame)); + } } + ui_stall_progress(UIStallPhase::AFTER_WATCHDOG, sm->frame); emit uiUpdate(*this, *starpilotUIState()); + ui_stall_progress(UIStallPhase::AFTER_EMIT, sm->frame); StarPilotUIState *fs = starpilotUIState(); StarPilotUIScene &starpilot_scene = fs->starpilot_scene; @@ -177,6 +373,8 @@ void UIState::update() { } fs->update(); + ui_stall_progress(UIStallPhase::AFTER_FS_UPDATE, sm->frame); + ui_stall_progress(UIStallPhase::IDLE, sm->frame); } Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) { diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index ea22b5399..78651d7b2 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -6,6 +6,7 @@ from openpilot.system.hardware import TICI from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.watchdog import kick_watchdog from openpilot.system.ui.lib.application import gui_app +from openpilot.selfdrive.ui.stall_monitor import UIStallMonitor from openpilot.selfdrive.ui.layouts.main import MainLayout from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout from openpilot.selfdrive.ui.ui_state import ui_state @@ -21,18 +22,33 @@ def main(): else: main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - for should_render in gui_app.render(): - kick_watchdog() - ui_state.update() - if should_render: - main_layout.render() + stall_monitor = UIStallMonitor("raylib_ui") + gui_app.set_progress_hook(stall_monitor.progress) + stall_monitor.progress("ui.loop_ready") + stall_monitor.start() - # reaffine after power save offlines our core - if TICI and os.sched_getaffinity(0) != cores: - try: - set_core_affinity(list(cores)) - except OSError: - pass + try: + for should_render in gui_app.render(): + stall_monitor.progress("ui.loop_iteration") + kick_watchdog() + stall_monitor.progress("ui.after_watchdog") + ui_state.update() + stall_monitor.progress("ui.after_state_update") + if should_render: + stall_monitor.progress("ui.before_layout_render") + main_layout.render() + stall_monitor.progress("ui.after_layout_render") + + # reaffine after power save offlines our core + if TICI and os.sched_getaffinity(0) != cores: + try: + set_core_affinity(list(cores)) + except OSError: + pass + stall_monitor.progress("ui.loop_idle") + finally: + gui_app.set_progress_hook(None) + stall_monitor.stop() if __name__ == "__main__": diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 6a6648ea6..90ec25a83 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -224,6 +224,7 @@ class GuiApplication: self._frame = 0 self._window_close_requested = False self._trace_log_callback = None + self._progress_hook: Callable[[str], None] | None = None self._modal_overlay = ModalOverlay() self._modal_overlay_shown = False self._modal_overlay_tick: Callable[[], None] | None = None @@ -261,6 +262,18 @@ class GuiApplication: def request_close(self): self._window_close_requested = True + def set_progress_hook(self, hook: Callable[[str], None] | None): + self._progress_hook = hook + + def _mark_progress(self, phase: str): + if self._progress_hook is None: + return + + try: + self._progress_hook(phase) + except Exception: + pass + def init_window(self, title: str, fps: int = _DEFAULT_FPS): with self._startup_profile_context(): @@ -536,6 +549,7 @@ class GuiApplication: self._render_profiler.enable() while not (self._window_close_requested or rl.window_should_close()): + self._mark_progress("gui_app.loop_start") if PC: # Thread is not used on PC, need to manually add mouse events self._mouse._handle_mouse_event() @@ -547,6 +561,7 @@ class GuiApplication: # Skip rendering when screen is off if not self._should_render: + self._mark_progress("gui_app.skip_render") if PC: rl.poll_input_events() time.sleep(1 / self._target_fps) @@ -554,25 +569,32 @@ class GuiApplication: continue if self._render_texture: + self._mark_progress("gui_app.begin_texture_mode") rl.begin_texture_mode(self._render_texture) rl.clear_background(rl.BLACK) else: + self._mark_progress("gui_app.begin_drawing") rl.begin_drawing() rl.clear_background(rl.BLACK) # Handle modal overlay rendering and input processing if self._render_nav_stack(): + self._mark_progress("gui_app.nav_stack") yield False elif self._handle_modal_overlay(): # Allow a Widget to still run a function while overlay is shown if self._modal_overlay_tick is not None: self._modal_overlay_tick() + self._mark_progress("gui_app.modal_overlay") yield False else: + self._mark_progress("gui_app.frame_ready") yield True if self._render_texture: + self._mark_progress("gui_app.end_texture_mode") rl.end_texture_mode() + self._mark_progress("gui_app.begin_present") rl.begin_drawing() rl.clear_background(rl.BLACK) src_rect = rl.Rectangle(0, 0, float(self._width), -float(self._height)) @@ -595,7 +617,9 @@ class GuiApplication: if self._grid_size > 0: self._draw_grid() + self._mark_progress("gui_app.end_drawing") rl.end_drawing() + self._mark_progress("gui_app.after_end_drawing") if RECORD: image = rl.load_image_from_texture(self._render_texture.texture) @@ -607,6 +631,7 @@ class GuiApplication: self._monitor_fps() self._frame += 1 + self._mark_progress("gui_app.frame_complete") if self._profile_render_frames > 0 and self._frame >= self._profile_render_frames: self._output_render_profile()