From 5ba6beed17baf17081169d2e1d5b144df00cda76 Mon Sep 17 00:00:00 2001 From: Andrew Valish Date: Mon, 9 Mar 2020 19:13:32 -0700 Subject: [PATCH] Refactor frame (#1192) * start drawing new sidebar, add assets * add thermal to ui, draw network_type and battery * draw sidebar metrics, add freeSpace and paTemp * draw static panda metric and network strength, start ubloxGnss messaging * use array for network_img * start sidebar touch events * prevent multiple touch events with touch_timeout * filter old touches, isolate sidebar events * add hwType check with timeout for panda metric * cleanup touch poll, handle vision touch, remove frame and black apks * cleanup per willem comments * update offroad, only read active_app from cereal * tweak sidebar behavior, show active app status * update offroad apk * read networkstrength from thermal in sidebar --- apk/ai.comma.plus.black.apk | 3 - apk/ai.comma.plus.frame.apk | 3 - apk/ai.comma.plus.offroad.apk | 4 +- common/apk.py | 7 +- selfdrive/assets/images/battery.png | Bin 0 -> 1704 bytes selfdrive/assets/images/battery_charging.png | Bin 0 -> 2131 bytes selfdrive/assets/images/button_home.png | Bin 0 -> 1501 bytes selfdrive/assets/images/button_settings.png | Bin 0 -> 957 bytes selfdrive/assets/images/network_0.png | Bin 0 -> 416 bytes selfdrive/assets/images/network_1.png | Bin 0 -> 462 bytes selfdrive/assets/images/network_2.png | Bin 0 -> 593 bytes selfdrive/assets/images/network_3.png | Bin 0 -> 524 bytes selfdrive/assets/images/network_4.png | Bin 0 -> 541 bytes selfdrive/assets/images/network_5.png | Bin 0 -> 503 bytes selfdrive/manager.py | 6 +- selfdrive/test/test_openpilot.py | 4 +- selfdrive/ui/SConscript | 2 +- selfdrive/ui/paint.cc | 20 ++ selfdrive/ui/sidebar.cc | 234 +++++++++++++++++++ selfdrive/ui/ui.cc | 178 +++++++++----- selfdrive/ui/ui.hpp | 39 +++- 21 files changed, 427 insertions(+), 73 deletions(-) delete mode 100644 apk/ai.comma.plus.black.apk delete mode 100644 apk/ai.comma.plus.frame.apk create mode 100644 selfdrive/assets/images/battery.png create mode 100644 selfdrive/assets/images/battery_charging.png create mode 100644 selfdrive/assets/images/button_home.png create mode 100644 selfdrive/assets/images/button_settings.png create mode 100644 selfdrive/assets/images/network_0.png create mode 100644 selfdrive/assets/images/network_1.png create mode 100644 selfdrive/assets/images/network_2.png create mode 100644 selfdrive/assets/images/network_3.png create mode 100644 selfdrive/assets/images/network_4.png create mode 100644 selfdrive/assets/images/network_5.png create mode 100644 selfdrive/ui/sidebar.cc diff --git a/apk/ai.comma.plus.black.apk b/apk/ai.comma.plus.black.apk deleted file mode 100644 index e0d0df3d96..0000000000 --- a/apk/ai.comma.plus.black.apk +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:309b46b7c38f10da52b18b0340eb3c57b633558a9a27c3ca4116474969ebb456 -size 84675 diff --git a/apk/ai.comma.plus.frame.apk b/apk/ai.comma.plus.frame.apk deleted file mode 100644 index 362a7cc747..0000000000 --- a/apk/ai.comma.plus.frame.apk +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d563bc9d43408f78d859538b50999cd7703487bdbe1fba1b11a3810205cd26c4 -size 2856327 diff --git a/apk/ai.comma.plus.offroad.apk b/apk/ai.comma.plus.offroad.apk index 172c99394c..3338981432 100644 --- a/apk/ai.comma.plus.offroad.apk +++ b/apk/ai.comma.plus.offroad.apk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf9cdba9b1f10d7b76573d1f78d208bc58a7273ae5d5e6f171be6c00c67863a6 -size 13678787 +oid sha256:deb70c5a5b284e73d4960b60ac7a6dcc1263597762554749adf675c23efc383e +size 13688133 diff --git a/common/apk.py b/common/apk.py index d77b83f141..5716ed452a 100644 --- a/common/apk.py +++ b/common/apk.py @@ -6,7 +6,7 @@ import shutil from common.basedir import BASEDIR from selfdrive.swaglog import cloudlog -android_packages = ("ai.comma.plus.offroad", "ai.comma.plus.frame") +android_packages = ("ai.comma.plus.offroad",) def get_installed_apks(): dat = subprocess.check_output(["pm", "list", "packages", "-f"], encoding='utf8').strip().split("\n") @@ -26,9 +26,9 @@ def install_apk(path): os.remove(install_path) return ret == 0 -def start_frame(): +def start_offroad(): set_package_permissions() - system("am start -n ai.comma.plus.frame/.MainActivity") + system("am start -n ai.comma.plus.offroad/.MainActivity") def set_package_permissions(): pm_grant("ai.comma.plus.offroad", "android.permission.ACCESS_FINE_LOCATION") @@ -95,4 +95,3 @@ def pm_apply_packages(cmd): if __name__ == "__main__": update_apks() - diff --git a/selfdrive/assets/images/battery.png b/selfdrive/assets/images/battery.png new file mode 100644 index 0000000000000000000000000000000000000000..cac3b9096a756e267a1090fcbf988268ccb2dc67 GIT binary patch literal 1704 zcmbVMU1%It6dtWglM;xHU{Whwr`1@~+4CyOnjlu}cl1fky9KNE?l9k{#q z-t*ma&UgNfjtnO|+IO@w4AYSsjHl_pL09;`E%f*Ixq%xDb3ZgQV`NMlg1TJ_Aj6)( zflA4ta)#Nrzv3W$5)&|i^QIMHZ!NE|z%(N4p^zqMP7D{!!C4oN&JJhv*-2eB*!@p~ zeHBO*lrTY{QYu;=tVGzFUP$-;HP3 zZ0bv?)H30b#585xh~1`WFEis{K8-zl%GGgVn!34*1%3qJGp-J zaGb6PD+!Xo2_hF1GGY)0B`AuV5Qc(KhibNA=4RGIB~BDGG~YoPipmBktsnyt^sit; zhdJ9VAxg?Dp*-dtE6;*fs9?-4+AcLry$i0tPsL&*uAMWBRN$qP2S6$jQ$$r!I4MwP zrfD!`c?4NHPQ@cE72KOY^Sz z*OitFm=;RCDmG1Z9p>3NQbsP0=4q^&(_*CA3?LTsr-KTf)R1mkeuwO7I;aZU6ZXF08KcZD*m=L&gh{v5V1v7yL8-<&`y7(wmE~(dJMCz%U|~} zCzrbEH`Z`=&KHr9v)?S-0JHK*=xUDeCNSE#QeIC3NEJN(ahkb zo8LaxesA=l&(_+`MyuJy?hofqJ^U;4+`xC+wm$LZxO9Ged$sr2yqR4D-SKR$ytZ7u zojbR}7fv3z^Ts9pr=G4yPxbxq$n$;kA1wB29}WDnx&&UC-`R7vYvGfQsaIckuk$J< zp}8bj>Akg#+pf;a(XZZGEq_tCeLZzyajAFf?qdVre7d&#+UoIMQ9FKp@`QF!`D5Y; zi<5`WeT=eKPQTkJ>6bH?Pjr6Xesp`}`^-!4pGh1(^K$v-jUAaEx8(PJ`P%slKR-?h OF{#9G{OtqdFa8O&nnT9` literal 0 HcmV?d00001 diff --git a/selfdrive/assets/images/battery_charging.png b/selfdrive/assets/images/battery_charging.png new file mode 100644 index 0000000000000000000000000000000000000000..4c25566d07598695d39ca3a8d71a9e7e1cfea6a9 GIT binary patch literal 2131 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 B)3yKr literal 0 HcmV?d00001 diff --git a/selfdrive/assets/images/button_settings.png b/selfdrive/assets/images/button_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..e04262b887cea54cce4fd822a35965e91a53b75e GIT binary patch literal 957 zcmeAS@N?(olHy`uVBq!ia0vp^D}gwJgBeKf-LJp{q*&4&eH|GXHuiJ>Nn{1`8H1)YfonjlyblP8z0aBxd`=ez%?SE$v2oU=VGM-+OV zPrDc(Jtgv8uR`XFD-RCsENFi9tx`%Xiow`A=D1l!T;g%wd6W5c9sRRieqWIK^@CH^ zsais5dFZPPN?T$CQWyGeki6>G9=7m4{{ewpM=xx?!3Ngvct)D?-yxFEHCF)m94jXv|V?y!oDa+%i21A zl}CY>E>utac_xB6_ed7K1?}WXa{7e@Pk7ni>6-8E-sk&bomG@tzboy}qUX0P&;H(8&FZRcyfpA&MGdE8 z;m_(tQ*Hm7^4<=;pZux3a9ZR!6P<14F*@S0I@y)~{xb$oeN20)hff&mmj(EWqHm_o7Z1XRC}(DlwB5;B33$aN%RXY zpWNxvr;|6X+I-wN^yby)43mC7H=cU4a}Jvz|MH`l>*6@A+uyP)R<3w{{_FBkn|hO3&yK99x@a-T4)%Z>8@E=bimnqV(Y9y91XE zo0oI1bV?ULF{{euZc&Amz{iqOrB_pISN)#RR4)Jc<%?sNRr@nOF`PGHU&JZt66{`~ zwo}0OxZ*5}BgRKNGmNG^m}%x_a(4&2r=>Cr>|28ttT zn4bnOQxei|()?MPHf0owC9ONS#cq{%*dd$8d+A~024Alyy}&j^RC9!gTu!>W77oSd zn&5I7a09})RB$mMX#t{=TR}45$;ZrcQbBzzbzV_%!G@$E9B?6rM2wbaAxTBWez}i}+#ssxomcLb&iJl)2JecTFQKP<3H<@|$5s!nMeIHR0000< KMNUMnLSTXug}12y literal 0 HcmV?d00001 diff --git a/selfdrive/assets/images/network_1.png b/selfdrive/assets/images/network_1.png new file mode 100644 index 0000000000000000000000000000000000000000..d7ae713f9090ce21671fa3149b0a58d383052281 GIT binary patch literal 462 zcmeAS@N?(olHy`uVBq!ia0vp^8-Q4vg&9cdIY~GGDU|@95LfpCpZ|n_NL6e)P(61^ zkY6x^yb!lk(IEo?QHwK6zDUU)m>00bgeN4^ z;4tHh%u7oS@_zSlc4l_>m^?G3&1w4MW}Y`Y7aG-_ApZ z+Fwq&U&-NR=~8R03%zeTncDJYn51Bo!Cc8a#zEQLDt7b?e*PQ4Rn(ktf ztxk?&|(O3I^?n!*v z@_%(pqc_9qPlps+j^x@rcX{mECKSY{+bQfTZ1zNl`RUU7^SHA^lOD!Di2Jm=aP3yySzE~y3+gk j!|hvc>~?ylTEaTTM(1`)R!$Kxz!^MU{an^LB{Ts5oA%uZ literal 0 HcmV?d00001 diff --git a/selfdrive/assets/images/network_2.png b/selfdrive/assets/images/network_2.png new file mode 100644 index 0000000000000000000000000000000000000000..17ecd977f1ead753ee24a8a0b201c0fec61e5b8f GIT binary patch literal 593 zcmeAS@N?(olHy`uVBq!ia0vp^8-Q4vg&9cdIY~GGDT4r?5ZC|z|GO9X07)W&vT~?9 zP%nQ;kYBJogCMsA)A#2~gOl6N)by{Gou-StI6M2i ze~Va2QKl=a`fK)c*Yfz9G&uOg#MKvCa5Z&wN56cu?te?(qxc6)RySXW3V0k_v~$l% z*&AN(TchhLKdHQ*9V29*KTXfi@BHZ=mv_$|KKM9G(lGqsuO`Q=%O}=^Y8F1%ju2I< zxSun(x-jI*+jF!1Z++k5%~*FvV2i59Dk<$xJ;$YEjw}?r<8^80r4=laRvTj+E^Smg z|D%=tXb+6esUMvi$?9e#-z+}tWeIhv-mpCuUT&qYmxsvfqm+|f-~jk&ht5B>XH#89rz^IYggIVD}TI1ruOzKck#?J zxBtwVY!f6q%iL(}qmPqt(`*A6QRM*%G$_|fa@Tu4?|3i0NjK(BzoUEi_U@*y|JQjuQ~kp2(9)7}{ZfSBB&hJ&4tUW(@XS@+~zVn8wjW^n-s$ObT=_Zk-&1l(r|8n7du7?o~YD<#dSv&6WW8FJt zapzx8{USBZq~mdi?|>blHtr*HK)< zn-vqT9$V$&y1vkO)|RGY-a$S`W`C1RG`Ia;F8xmDmdKI;Vst0G}iBF>CA3 z|MhQ+-mO*We6sP(XA5H$#=zu%4aK*TJG`R~b~Kpk>R+0dHpNqZspHvN;HD$U!Sv?@Kl9;PeEudpm$B9!+?ZhnkwmvQ!|3mz-~7+lv_ zRkPSQ=l7mm5A(dxhiAOrep&HHVLs!h&pl149=VS?6PMP-9b13)x37(G`^xz(FKk5n z9sZUAbr%0H;a1XquBA_@f81&0~%(#v!2AN zsJd?M{PwRPv@VW^Cryxbn{$G&XZJ;&)3Pd4=g%;&u6(BFVdQ&MBb@0Ll~+4FCWD literal 0 HcmV?d00001 diff --git a/selfdrive/assets/images/network_5.png b/selfdrive/assets/images/network_5.png new file mode 100644 index 0000000000000000000000000000000000000000..fba67a95a5a786a95c0cd4d6f1009706b4630e60 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0vp^8-Q4vgBeJ^-t^TGNZAMYgt)pF`242;c=gVb02(Dy z666=mAl_dc^z_|#-UX)<+5WU;ZawFy7HWNaH3I`!dcLC`-)fUh7Yf631V5h-ef_yFw>eg|TmAEt#RL?S0#|!1 z)R*vH$j_xC=dGijtQZ>pHao9v_U*%G_o~jS<~RSobn?EpKhCFj7R47Y-}>y_;kS1! wZtcDtTakS%JMV1$zj&tg|MV|jm_I+h*6D_z%++;*z~E=_boFyt=akR{0FSEppa1{> literal 0 HcmV?d00001 diff --git a/selfdrive/manager.py b/selfdrive/manager.py index 73347af872..d39c8701b9 100755 --- a/selfdrive/manager.py +++ b/selfdrive/manager.py @@ -129,7 +129,7 @@ from selfdrive.version import version, dirty from selfdrive.loggerd.config import ROOT from selfdrive.launcher import launcher from common import android -from common.apk import update_apks, pm_apply_packages, start_frame +from common.apk import update_apks, pm_apply_packages, start_offroad from common.manager_helpers import print_cpu_usage ThermalStatus = cereal.log.ThermalData.ThermalStatus @@ -406,10 +406,10 @@ def manager_thread(): for p in persistent_processes: start_managed_process(p) - # start frame + # start offroad if ANDROID: pm_apply_packages('enable') - start_frame() + start_offroad() if os.getenv("NOBOARD") is None: start_managed_process("pandad") diff --git a/selfdrive/test/test_openpilot.py b/selfdrive/test/test_openpilot.py index e5ea327369..3e9a317582 100644 --- a/selfdrive/test/test_openpilot.py +++ b/selfdrive/test/test_openpilot.py @@ -2,7 +2,7 @@ import os os.environ['FAKEUPLOAD'] = "1" -from common.apk import update_apks, start_frame, pm_apply_packages, android_packages +from common.apk import update_apks, start_offroad, pm_apply_packages, android_packages from common.params import Params from common.testing import phone_only from selfdrive.manager import manager_init, manager_prepare @@ -57,7 +57,7 @@ def with_apks(): update_apks() pm_apply_packages('enable') - start_frame() + start_offroad() func() diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 44ce4e332d..e0c353b43e 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -1,6 +1,6 @@ Import('env', 'arch', 'common', 'messaging', 'gpucommon', 'visionipc', 'cereal') -src = ['ui.cc', 'paint.cc', '#phonelibs/nanovg/nanovg.c'] +src = ['ui.cc', 'paint.cc', 'sidebar.cc', '#phonelibs/nanovg/nanovg.c'] libs = [common, 'zmq', 'czmq', 'capnp', 'capnp_c', 'm', cereal, messaging, gpucommon, visionipc] if arch == "aarch64": diff --git a/selfdrive/ui/paint.cc b/selfdrive/ui/paint.cc index 7e8fda20ac..ce854ecbe1 100644 --- a/selfdrive/ui/paint.cc +++ b/selfdrive/ui/paint.cc @@ -877,6 +877,7 @@ static void ui_draw_blank(UIState *s) { } void ui_draw(UIState *s) { + ui_draw_sidebar(s); if (s->vision_connected && s->active_app == cereal_UiLayoutState_App_home && s->status != STATUS_STOPPED) { ui_draw_vision(s); } else { @@ -985,6 +986,25 @@ void ui_nvg_init(UIState *s) { assert(s->img_map >= 0); s->img_map = nvgCreateImage(s->vg, "../assets/img_map.png", 1); + assert(s->img_button_settings >= 0); + s->img_button_settings = nvgCreateImage(s->vg, "../assets/images/button_settings.png", 1); + + assert(s->img_button_home >= 0); + s->img_button_home = nvgCreateImage(s->vg, "../assets/images/button_home.png", 1); + + assert(s->img_battery >= 0); + s->img_battery = nvgCreateImage(s->vg, "../assets/images/battery.png", 1); + + assert(s->img_battery_charging >= 0); + s->img_battery_charging = nvgCreateImage(s->vg, "../assets/images/battery_charging.png", 1); + + for(int i=0;i<=5;++i) { + assert(s->img_network[i] >= 0); + char network_asset[32]; + snprintf(network_asset, sizeof(network_asset), "../assets/images/network_%d.png", i); + s->img_network[i] = nvgCreateImage(s->vg, network_asset, 1); + } + // init gl s->frame_program = load_program(frame_vertex_shader, frame_fragment_shader); assert(s->frame_program); diff --git a/selfdrive/ui/sidebar.cc b/selfdrive/ui/sidebar.cc new file mode 100644 index 0000000000..717449dee9 --- /dev/null +++ b/selfdrive/ui/sidebar.cc @@ -0,0 +1,234 @@ +#include +#include +#include +#include "ui.hpp" + +static void ui_draw_sidebar_background(UIState *s, bool hasSidebar) { + int sbr_x = hasSidebar ? 0 : -(sbr_w) + bdr_s * 2; + + nvgBeginPath(s->vg); + nvgRect(s->vg, sbr_x, 0, sbr_w, vwp_h); + nvgFillColor(s->vg, COLOR_BLACK_ALPHA); + nvgFill(s->vg); +} + +static void ui_draw_sidebar_settings_button(UIState *s, bool hasSidebar) { + bool settingsActive = s->active_app == cereal_UiLayoutState_App_settings; + const int settings_btn_xr = hasSidebar ? settings_btn_x : -(sbr_w); + + nvgBeginPath(s->vg); + NVGpaint imgPaint = nvgImagePattern(s->vg, settings_btn_xr, settings_btn_y, + settings_btn_w, settings_btn_h, 0, s->img_button_settings, settingsActive ? 1.0f : 0.65f); + nvgRect(s->vg, settings_btn_xr, settings_btn_y, settings_btn_w, settings_btn_h); + nvgFillPaint(s->vg, imgPaint); + nvgFill(s->vg); +} + +static void ui_draw_sidebar_home_button(UIState *s, bool hasSidebar) { + bool homeActive = s->active_app == cereal_UiLayoutState_App_home; + const int home_btn_xr = hasSidebar ? home_btn_x : -(sbr_w); + + nvgBeginPath(s->vg); + NVGpaint imgPaint = nvgImagePattern(s->vg, home_btn_xr, home_btn_y, + home_btn_w, home_btn_h, 0, s->img_button_home, homeActive ? 1.0f : 0.65f); + nvgRect(s->vg, home_btn_xr, home_btn_y, home_btn_w, home_btn_h); + nvgFillPaint(s->vg, imgPaint); + nvgFill(s->vg); +} + +static void ui_draw_sidebar_network_strength(UIState *s, bool hasSidebar) { + const int network_img_h = 27; + const int network_img_w = 176; + const int network_img_x = hasSidebar ? 58 : -(sbr_w); + const int network_img_y = 196; + const int network_img = s->scene.networkType == cereal_ThermalData_NetworkType_none ? + s->img_network[0] : s->img_network[s->scene.networkStrength + 1]; + + nvgBeginPath(s->vg); + NVGpaint imgPaint = nvgImagePattern(s->vg, network_img_x, network_img_y, + network_img_w, network_img_h, 0, network_img, 1.0f); + nvgRect(s->vg, network_img_x, network_img_y, network_img_w, network_img_h); + nvgFillPaint(s->vg, imgPaint); + nvgFill(s->vg); +} + +static void ui_draw_sidebar_battery_icon(UIState *s, bool hasSidebar) { + const int battery_img_h = 36; + const int battery_img_w = 76; + const int battery_img_x = hasSidebar ? 160 : -(sbr_w); + const int battery_img_y = 255; + + int battery_img = strcmp(s->scene.batteryStatus, "Charging") == 0 ? + s->img_battery_charging : s->img_battery; + + nvgBeginPath(s->vg); + nvgRect(s->vg, battery_img_x + 6, battery_img_y + 5, + ((battery_img_w - 19) * (s->scene.batteryPercent * 0.01)), battery_img_h - 11); + nvgFillColor(s->vg, COLOR_WHITE); + nvgFill(s->vg); + + nvgBeginPath(s->vg); + NVGpaint imgPaint = nvgImagePattern(s->vg, battery_img_x, battery_img_y, + battery_img_w, battery_img_h, 0, battery_img, 1.0f); + nvgRect(s->vg, battery_img_x, battery_img_y, battery_img_w, battery_img_h); + nvgFillPaint(s->vg, imgPaint); + nvgFill(s->vg); +} + +static void ui_draw_sidebar_network_type(UIState *s, bool hasSidebar) { + const int network_x = hasSidebar ? 50 : -(sbr_w); + const int network_y = 273; + const int network_w = 100; + const int network_h = 100; + const char *network_types[6] = {"--", "WiFi", "2G", "3G", "4G", "5G"}; + char network_type_str[32]; + + if (s->scene.networkType <= 5) { + snprintf(network_type_str, sizeof(network_type_str), "%s", network_types[s->scene.networkType]); + } + + nvgFillColor(s->vg, COLOR_WHITE); + nvgFontSize(s->vg, 48); + nvgFontFace(s->vg, "sans-regular"); + nvgTextAlign(s->vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); + nvgTextBox(s->vg, network_x, network_y, network_w, network_type_str, NULL); +} + +static void ui_draw_sidebar_metric(UIState *s, const char* label_str, const char* value_str, const int severity, const int y_offset, const char* message_str, bool hasSidebar) { + const int metric_x = hasSidebar ? 30 : -(sbr_w); + const int metric_y = 338 + y_offset; + const int metric_w = 240; + const int metric_h = message_str ? strlen(message_str) > 8 ? 124 : 100 : 148; + NVGcolor status_color; + + if (severity == 0) { + status_color = COLOR_WHITE; + } else if (severity == 1) { + status_color = COLOR_YELLOW; + } else if (severity > 1) { + status_color = COLOR_RED; + } + + nvgBeginPath(s->vg); + nvgRoundedRect(s->vg, metric_x, metric_y, metric_w, metric_h, 20); + nvgStrokeColor(s->vg, severity > 0 ? COLOR_WHITE : COLOR_WHITE_ALPHA); + nvgStrokeWidth(s->vg, 2); + nvgStroke(s->vg); + + nvgBeginPath(s->vg); + nvgRoundedRectVarying(s->vg, metric_x + 6, metric_y + 6, 18, metric_h - 12, 25, 0, 0, 25); + nvgFillColor(s->vg, status_color); + nvgFill(s->vg); + + if (!message_str) { + nvgFillColor(s->vg, COLOR_WHITE); + nvgFontSize(s->vg, 78); + nvgFontFace(s->vg, "sans-bold"); + nvgTextAlign(s->vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + nvgTextBox(s->vg, metric_x + 50, metric_y + 50, metric_w - 60, value_str, NULL); + + nvgFillColor(s->vg, COLOR_WHITE); + nvgFontSize(s->vg, 48); + nvgFontFace(s->vg, "sans-regular"); + nvgTextAlign(s->vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + nvgTextBox(s->vg, metric_x + 50, metric_y + 50 + 66, metric_w - 60, label_str, NULL); + } else { + nvgFillColor(s->vg, COLOR_WHITE); + nvgFontSize(s->vg, 48); + nvgFontFace(s->vg, "sans-bold"); + nvgTextAlign(s->vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); + nvgTextBox(s->vg, metric_x + 35, metric_y + (strlen(message_str) > 8 ? 40 : 50), metric_w - 50, message_str, NULL); + } +} + +static void ui_draw_sidebar_storage_metric(UIState *s, bool hasSidebar) { + int storage_severity; + char storage_label_str[32]; + char storage_value_str[32]; + char storage_value_unit[32]; + const int storage_y_offset = 0; + const float storage_pct = ceilf((1.0 - s->scene.freeSpace) * 100); + + if (storage_pct < 75.0) { + storage_severity = 0; + } else if (storage_pct >= 75.0 && storage_pct < 87.0) { + storage_severity = 1; + } else if (storage_pct >= 87.0) { + storage_severity = 2; + } + + snprintf(storage_value_str, sizeof(storage_value_str), "%d", (int)storage_pct); + snprintf(storage_value_unit, sizeof(storage_value_unit), "%s", "%"); + snprintf(storage_label_str, sizeof(storage_label_str), "%s", "STORAGE"); + strcat(storage_value_str, storage_value_unit); + + ui_draw_sidebar_metric(s, storage_label_str, storage_value_str, storage_severity, storage_y_offset, NULL, hasSidebar); +} + +static void ui_draw_sidebar_temp_metric(UIState *s, bool hasSidebar) { + int temp_severity; + char temp_label_str[32]; + char temp_value_str[32]; + char temp_value_unit[32]; + const int temp_y_offset = 148 + 32; + + if (s->scene.thermalStatus == cereal_ThermalData_ThermalStatus_green) { + temp_severity = 0; + } else if (s->scene.thermalStatus == cereal_ThermalData_ThermalStatus_yellow) { + temp_severity = 1; + } else if (s->scene.thermalStatus == cereal_ThermalData_ThermalStatus_red) { + temp_severity = 2; + } else if (s->scene.thermalStatus == cereal_ThermalData_ThermalStatus_danger) { + temp_severity = 3; + } + + snprintf(temp_value_str, sizeof(temp_value_str), "%d", s->scene.paTemp); + snprintf(temp_value_unit, sizeof(temp_value_unit), "%s", "°C"); + snprintf(temp_label_str, sizeof(temp_label_str), "%s", "TEMP"); + strcat(temp_value_str, temp_value_unit); + + ui_draw_sidebar_metric(s, temp_label_str, temp_value_str, temp_severity, temp_y_offset, NULL, hasSidebar); +} + +static void ui_draw_sidebar_panda_metric(UIState *s, bool hasSidebar) { + int panda_severity; + char panda_message_str[32]; + const int panda_y_offset = (148 + 32) * 2; + + if (s->scene.hwType == cereal_HealthData_HwType_unknown) { + panda_severity = 2; + snprintf(panda_message_str, sizeof(panda_message_str), "%s", "NO PANDA"); + } else if (s->scene.hwType == cereal_HealthData_HwType_whitePanda) { + panda_severity = 0; + snprintf(panda_message_str, sizeof(panda_message_str), "%s", "PANDA ACTIVE"); + } else if ( + (s->scene.hwType == cereal_HealthData_HwType_greyPanda) || + (s->scene.hwType == cereal_HealthData_HwType_blackPanda) || + (s->scene.hwType == cereal_HealthData_HwType_uno)) { + if (s->scene.satelliteCount == -1) { + panda_severity = 0; + snprintf(panda_message_str, sizeof(panda_message_str), "%s", "PANDA ACTIVE"); + } else if (s->scene.satelliteCount < 6) { + panda_severity = 1; + snprintf(panda_message_str, sizeof(panda_message_str), "%s", "PANDA\nNO GPS"); + } else if (s->scene.satelliteCount >= 6) { + panda_severity = 0; + snprintf(panda_message_str, sizeof(panda_message_str), "%s", "PANDA GOOD GPS"); + } + } + + ui_draw_sidebar_metric(s, NULL, NULL, panda_severity, panda_y_offset, panda_message_str, hasSidebar); +} + +void ui_draw_sidebar(UIState *s) { + bool hasSidebar = !s->scene.uilayout_sidebarcollapsed; + ui_draw_sidebar_background(s, hasSidebar); + ui_draw_sidebar_settings_button(s, hasSidebar); + ui_draw_sidebar_home_button(s, hasSidebar); + ui_draw_sidebar_network_strength(s, hasSidebar); + ui_draw_sidebar_battery_icon(s, hasSidebar); + ui_draw_sidebar_network_type(s, hasSidebar); + ui_draw_sidebar_storage_metric(s, hasSidebar); + ui_draw_sidebar_temp_metric(s, hasSidebar); + ui_draw_sidebar_panda_metric(s, hasSidebar); +} diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index da566a1e8a..7b00117921 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -57,6 +57,45 @@ static void set_awake(UIState *s, bool awake) { #endif } +static void navigate_to_settings(UIState *s) { +#ifdef QCOM + system("am broadcast -a 'ai.comma.plus.SidebarSettingsTouchUpInside'"); +#else + // computer UI doesn't have offroad settings +#endif +} + +static void navigate_to_home(UIState *s) { +#ifdef QCOM + system("am broadcast -a 'ai.comma.plus.HomeButtonTouchUpInside'"); +#else + // computer UI doesn't have offroad home +#endif +} + +static void handle_sidebar_touch(UIState *s, int touch_x, int touch_y) { + if (!s->scene.uilayout_sidebarcollapsed && touch_x <= sbr_w) { + if (touch_x >= settings_btn_x && touch_x < (settings_btn_x + settings_btn_w) + && touch_y >= settings_btn_y && touch_y < (settings_btn_y + settings_btn_h)) { + navigate_to_settings(s); + } + if (touch_x >= home_btn_x && touch_x < (home_btn_x + home_btn_w) + && touch_y >= home_btn_y && touch_y < (home_btn_y + home_btn_h)) { + navigate_to_home(s); + if (s->vision_connected) { + s->scene.uilayout_sidebarcollapsed = true; + } + } + } +} + +static void handle_vision_touch(UIState *s, int touch_x, int touch_y) { + if (s->vision_connected && (touch_x >= s->scene.ui_viz_rx - bdr_s) + && (s->active_app != cereal_UiLayoutState_App_settings)) { + s->scene.uilayout_sidebarcollapsed = !s->scene.uilayout_sidebarcollapsed; + } +} + volatile sig_atomic_t do_exit = 0; static void set_do_exit(int sig) { do_exit = 1; @@ -110,19 +149,28 @@ static void ui_init(UIState *s) { s->uilayout_sock = SubSocket::create(s->ctx, "uiLayoutState"); s->livecalibration_sock = SubSocket::create(s->ctx, "liveCalibration"); s->radarstate_sock = SubSocket::create(s->ctx, "radarState"); + s->thermal_sock = SubSocket::create(s->ctx, "thermal"); + s->health_sock = SubSocket::create(s->ctx, "health"); + s->ubloxgnss_sock = SubSocket::create(s->ctx, "ubloxGnss"); assert(s->model_sock != NULL); assert(s->controlsstate_sock != NULL); assert(s->uilayout_sock != NULL); assert(s->livecalibration_sock != NULL); assert(s->radarstate_sock != NULL); + assert(s->thermal_sock != NULL); + assert(s->health_sock != NULL); + assert(s->ubloxgnss_sock != NULL); s->poller = Poller::create({ s->model_sock, s->controlsstate_sock, s->uilayout_sock, s->livecalibration_sock, - s->radarstate_sock + s->radarstate_sock, + s->thermal_sock, + s->health_sock, + s->ubloxgnss_sock }); #ifdef SHOW_SPEEDLIMIT @@ -404,27 +452,56 @@ void handle_message(UIState *s, Message * msg) { cereal_read_UiLayoutState(&datad, eventd.uiLayoutState); s->active_app = datad.activeApp; s->scene.uilayout_sidebarcollapsed = datad.sidebarCollapsed; - s->scene.uilayout_mapenabled = datad.mapEnabled; - - bool hasSidebar = !s->scene.uilayout_sidebarcollapsed; - bool mapEnabled = s->scene.uilayout_mapenabled; - if (mapEnabled) { - s->scene.ui_viz_rx = hasSidebar ? (box_x+nav_w) : (box_x+nav_w-(bdr_s*4)); - s->scene.ui_viz_rw = hasSidebar ? (box_w-nav_w) : (box_w-nav_w+(bdr_s*4)); - s->scene.ui_viz_ro = -(sbr_w + 4*bdr_s); - } else { - s->scene.ui_viz_rx = hasSidebar ? box_x : (box_x-sbr_w+bdr_s*2); - s->scene.ui_viz_rw = hasSidebar ? box_w : (box_w+sbr_w-(bdr_s*2)); - s->scene.ui_viz_ro = hasSidebar ? -(sbr_w - 6*bdr_s) : 0; - } } else if (eventd.which == cereal_Event_liveMapData) { struct cereal_LiveMapData datad; cereal_read_LiveMapData(&datad, eventd.liveMapData); s->scene.map_valid = datad.mapValid; + } else if (eventd.which == cereal_Event_thermal) { + struct cereal_ThermalData datad; + cereal_read_ThermalData(&datad, eventd.thermal); + + s->scene.networkType = datad.networkType; + s->scene.networkStrength = datad.networkStrength; + s->scene.batteryPercent = datad.batteryPercent; + snprintf(s->scene.batteryStatus, sizeof(s->scene.batteryStatus), "%s", datad.batteryStatus.str); + s->scene.freeSpace = datad.freeSpace; + s->scene.thermalStatus = datad.thermalStatus; + s->scene.paTemp = datad.pa0; + } else if (eventd.which == cereal_Event_ubloxGnss) { + struct cereal_UbloxGnss datad; + cereal_read_UbloxGnss(&datad, eventd.ubloxGnss); + struct cereal_UbloxGnss_MeasurementReport reportdatad; + cereal_read_UbloxGnss_MeasurementReport(&reportdatad, datad.measurementReport); + + s->scene.satelliteCount = reportdatad.numMeas; + } else if (eventd.which == cereal_Event_health) { + struct cereal_HealthData datad; + cereal_read_HealthData(&datad, eventd.health); + + s->scene.hwType = datad.hwType; + s->hardware_timeout = 5*30; // 5 seconds at 30 fps } capn_free(&ctx); } +static void check_messages(UIState *s) { + while(true) { + auto polls = s->poller->poll(0); + + if (polls.size() == 0) + break; + + for (auto sock : polls){ + Message * msg = sock->receive(); + if (msg == NULL) continue; + + handle_message(s, msg); + + delete msg; + } + } +} + static void ui_update(UIState *s) { int err; @@ -494,7 +571,7 @@ static void ui_update(UIState *s) { assert(glGetError() == GL_NO_ERROR); - // Default UI Measurements (Assumes sidebar collapsed) + s->scene.uilayout_sidebarcollapsed = true; s->scene.ui_viz_rx = (box_x-sbr_w+bdr_s*2); s->scene.ui_viz_rw = (box_w+sbr_w-(bdr_s*2)); s->scene.ui_viz_ro = 0; @@ -575,23 +652,7 @@ static void ui_update(UIState *s) { break; } // peek and consume all events in the zmq queue, then return. - while(true) { - auto polls = s->poller->poll(0); - - if (polls.size() == 0) - return; - - for (auto sock : polls){ - Message * msg = sock->receive(); - if (msg == NULL) continue; - - set_awake(s, true); - - handle_message(s, msg); - - delete msg; - } - } + check_messages(s); } static int vision_subscribe(int fd, VisionPacket *rp, VisionStreamType type) { @@ -732,7 +793,6 @@ fail: return NULL; } - static void* bg_thread(void* args) { UIState *s = (UIState*)args; set_thread_name("bg"); @@ -813,7 +873,6 @@ int main(int argc, char* argv[]) { TouchState touch = {0}; touch_init(&touch); s->touch_fd = touch.fd; - ui_sound_init(); // light sensor scaling params @@ -830,6 +889,9 @@ int main(int argc, char* argv[]) { set_volume(MIN_VOLUME); s->volume_timeout = 5 * UI_FREQ; int draws = 0; + + s->scene.satelliteCount = -1; + while (!do_exit) { bool should_swap = false; if (!s->vision_connected) { @@ -847,34 +909,37 @@ int main(int argc, char* argv[]) { if (smooth_brightness > 255) smooth_brightness = 255; set_brightness(s, (int)smooth_brightness); + // resize vision for collapsing sidebar + const bool hasSidebar = !s->scene.uilayout_sidebarcollapsed; + s->scene.ui_viz_rx = hasSidebar ? box_x : (box_x - sbr_w + (bdr_s * 2)); + s->scene.ui_viz_rw = hasSidebar ? box_w : (box_w + sbr_w - (bdr_s * 2)); + s->scene.ui_viz_ro = hasSidebar ? -(sbr_w - 6 * bdr_s) : 0; + + // poll for touch events + int touch_x = -1, touch_y = -1; + int touched = touch_poll(&touch, &touch_x, &touch_y, 0); + if (touched == 1) { + set_awake(s, true); + handle_sidebar_touch(s, touch_x, touch_y); + handle_vision_touch(s, touch_x, touch_y); + } + if (!s->vision_connected) { - // Car is not started, keep in idle state and awake on touch events - zmq_pollitem_t polls[1] = {{0}}; - polls[0].fd = s->touch_fd; - polls[0].events = ZMQ_POLLIN; - int ret = zmq_poll(polls, 1, 0); - if (ret < 0){ - if (errno == EINTR) continue; - LOGW("poll failed (%d)", ret); - } else if (ret > 0) { - // awake on any touch - int touch_x = -1, touch_y = -1; - int touched = touch_read(&touch, &touch_x, &touch_y); - if (touched == 1) { - set_awake(s, true); - } - } if (s->status != STATUS_STOPPED) { update_status(s, STATUS_STOPPED); } + check_messages(s); } else { + set_awake(s, true); if (s->status == STATUS_STOPPED) { update_status(s, STATUS_DISENGAGED); } // Car started, fetch a new rgb image from ipc and peek for zmq events. ui_update(s); - if(!s->vision_connected) { + if (!s->vision_connected) { // Visiond process is just stopped, force a redraw to make screen blank again. + s->scene.satelliteCount = -1; + s->scene.uilayout_sidebarcollapsed = false; ui_draw(s); glFinish(); should_swap = true; @@ -888,8 +953,15 @@ int main(int argc, char* argv[]) { set_awake(s, false); } - // Don't waste resources on drawing in case screen is off or car is not started. - if (s->awake && s->vision_connected) { + // manage hardware disconnect + if (s->hardware_timeout > 0) { + s->hardware_timeout--; + } else { + s->scene.hwType = cereal_HealthData_HwType_unknown; + } + + // Don't waste resources on drawing in case screen is off + if (s->awake) { ui_draw(s); glFinish(); should_swap = true; diff --git a/selfdrive/ui/ui.hpp b/selfdrive/ui/ui.hpp index 64d32c65c1..ee431a4496 100644 --- a/selfdrive/ui/ui.hpp +++ b/selfdrive/ui/ui.hpp @@ -38,6 +38,12 @@ #define ALERTSIZE_MID 2 #define ALERTSIZE_FULL 3 +#define COLOR_BLACK_ALPHA nvgRGBA(0, 0, 0, 85) +#define COLOR_WHITE nvgRGBA(255, 255, 255, 255) +#define COLOR_WHITE_ALPHA nvgRGBA(255, 255, 255, 85) +#define COLOR_YELLOW nvgRGBA(218, 202, 37, 255) +#define COLOR_RED nvgRGBA(201, 34, 49, 255) + #ifndef QCOM #define UI_60FPS #endif @@ -60,6 +66,14 @@ const int viz_w = vwp_w-(bdr_s*2); const int header_h = 420; const int footer_h = 280; const int footer_y = vwp_h-bdr_s-footer_h; +const int settings_btn_h = 117; +const int settings_btn_w = 200; +const int settings_btn_x = 50; +const int settings_btn_y = 35; +const int home_btn_h = 180; +const int home_btn_w = 180; +const int home_btn_x = 60; +const int home_btn_y = vwp_h - home_btn_h - 40; const int UI_FREQ = 30; // Hz @@ -115,7 +129,7 @@ typedef struct UIScene { int lead_status; float lead_d_rel, lead_y_rel, lead_v_rel; - + int lead_status2; float lead_d_rel2, lead_y_rel2, lead_v_rel2; @@ -131,6 +145,16 @@ typedef struct UIScene { // Used to show gps planner status bool gps_planner_active; + + uint8_t networkType; + uint8_t networkStrength; + int batteryPercent; + char batteryStatus[64]; + float freeSpace; + uint8_t thermalStatus; + int paTemp; + int hwType; + int satelliteCount; } UIScene; typedef struct { @@ -168,6 +192,11 @@ typedef struct UIState { int img_turn; int img_face; int img_map; + int img_button_settings; + int img_button_home; + int img_battery; + int img_battery_charging; + int img_network[6]; // sockets Context *ctx; @@ -177,7 +206,11 @@ typedef struct UIState { SubSocket *radarstate_sock; SubSocket *map_data_sock; SubSocket *uilayout_sock; + SubSocket *thermal_sock; + SubSocket *health_sock; + SubSocket *ubloxgnss_sock; Poller * poller; + Poller * ublox_poller; int active_app; @@ -221,6 +254,7 @@ typedef struct UIState { int is_metric_timeout; int longitudinal_control_timeout; int limit_set_speed_timeout; + int hardware_timeout; bool controls_seen; @@ -254,8 +288,9 @@ typedef struct UIState { // API void ui_draw_vision_alert(UIState *s, int va_size, int va_color, - const char* va_text1, const char* va_text2); + const char* va_text1, const char* va_text2); void ui_draw(UIState *s); +void ui_draw_sidebar(UIState *s); void ui_nvg_init(UIState *s); #endif