Merge branch 'master-priv' into disable-on-screen-recorder

This commit is contained in:
Jason Wen
2024-04-12 00:09:27 -04:00
64 changed files with 1088 additions and 275 deletions
+2 -2
View File
@@ -5,10 +5,10 @@
*.dlc filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
#*.png filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
#*.wav filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text
+14
View File
@@ -20,4 +20,18 @@
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
<tool name="Poetry SCons Build Debug - Jason Wen" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="false" showConsoleOnStdErr="false" synchronizeAfterRun="true">
<exec>
<option name="COMMAND" value="poetry" />
<option name="PARAMETERS" value="run scons -u -j20 --compile_db --ccflags=&quot;-fno-inline&quot;" />
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
<tool name="Poetry SCons Build Release - Jason Wen" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="false" showConsoleOnStdErr="false" synchronizeAfterRun="true">
<exec>
<option name="COMMAND" value="poetry" />
<option name="PARAMETERS" value="run scons -u -j20 --compile_db" />
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
</toolSet>
+2 -2
View File
@@ -1,4 +1,4 @@
[lfs]
url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs
pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git
url = https://gitlab.com/sunnypilot/public/sunnypilot-lfs.git/info/lfs
pushurl = ssh://git@gitlab.com/sunnypilot/public/sunnypilot-lfs.git
locksverify = false
+4
View File
@@ -0,0 +1,4 @@
[lfs]
url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs
pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git
locksverify = false
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build Debug" type="CLionExternalRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$/selfdrive/ui" PASS_PARENT_ENVS_2="true" PROJECT_NAME="openpilot-special" TARGET_NAME="Poetry SCons Build Debug" CONFIG_NAME="Poetry SCons Build Debug" RUN_PATH="_ui">
<configuration default="false" name="Build Debug" type="CLionExternalRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$/selfdrive/ui" PASS_PARENT_ENVS_2="true" PROJECT_NAME="openpilot-special" TARGET_NAME="Poetry SCons Build Debug" CONFIG_NAME="Poetry SCons Build Debug" RUN_PATH="ui">
<envs>
<env name="QT_DBL_CLICK_DIST" value="150" />
</envs>
+7 -2
View File
@@ -5,17 +5,22 @@ sunnypilot - 0.9.7.0 (2024-xx-xx)
************************
* UPDATED: Synced with commaai's openpilot
* master commit 56e343b (February 27, 2024)
* NEW❗: Config Backup (Alpha access only for GitHub Sponsors and Patreon supporters)
* NEW❗: Config Backup (Alpha early access)
* Remotely back up and restore sunnypilot settings easily
* Device registration with sunnylink ensures a secure, integrated experience across services
* AES encryption derived from the device's RSA private key is used for utmost security
* Settings are encrypted on-device, transmitted securely via HTTPS, and stored encrypted on sunnylink
* Prevents loss of settings after device resets, offering peace of mind through end-to-end encryption
* Early alpha access to all current and previous GitHub Sponsors and Patreon supporters
* Go to https://discord.gg/sunnypilot and reach out to one of the moderators to confirm your alpha access
* GitHub account pairing from device settings scanning QR code
* Pairing your account will allow you to access features via our API (still WIP but accessible if you dig a little on our code 😉)
* Allow inheritance of your sponsorship status, allowing you to get extra features and early access whenever applicable
* Immediate sponsor recognition works only for new sponsors. If you're an earlier sponsor, we've got you — just reach out to a moderator on Discord (https://discord.gg/sunnypilot) to get sorted!
* RE-ENABLED: Map-based Turn Speed Control (M-TSC) for supported platforms
* openpilot Longitudianl Control available cars
* Custom Stock Longitudinal Control available cars
* UPDATED: Reset Mapbox Access Token -> Reset Access Tokens for Map Services
* Reset self-service access tokens for Mapbox, Amap, and Google Maps
* UI Updates
* Display Metrics Below Chevron
* NEW❗: Metrics is now being displayed below the chevron instead of above
+19 -13
View File
@@ -20,6 +20,13 @@ class SunnylinkApi(BaseApi):
super().__init__(dongle_id, API_HOST)
self.user_agent = "sunnypilot-"
self.spinner = None
self.params = Params()
def api_get(self, endpoint, method='GET', timeout=10, **kwargs):
if not self.params.get_bool("SunnylinkEnabled"):
return None
return super().api_get(endpoint, method, timeout, **kwargs)
def get_token(self, expiry_hours=1):
# Add your additional data here
@@ -32,39 +39,38 @@ class SunnylinkApi(BaseApi):
self.spinner.update(message)
time.sleep(0.5)
def _resolve_dongle_ids(self, params):
sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8')
comma_dongle_id = self.dongle_id or params.get("DongleId", encoding='utf-8')
def _resolve_dongle_ids(self):
sunnylink_dongle_id = self.params.get("SunnylinkDongleId", encoding='utf-8')
comma_dongle_id = self.dongle_id or self.params.get("DongleId", encoding='utf-8')
return sunnylink_dongle_id, comma_dongle_id
def _resolve_imeis(self, params):
def _resolve_imeis(self):
imei1, imei2 = None, None
imei_try = 0
while imei1 is None and imei2 is None and imei_try < MAX_RETRIES:
try:
imei1, imei2 = params.get("IMEI", encoding='utf8') or HARDWARE.get_imei(0), HARDWARE.get_imei(1)
imei1, imei2 = self.params.get("IMEI", encoding='utf8') or HARDWARE.get_imei(0), HARDWARE.get_imei(1)
except Exception:
self._status_update(f"Error getting imei, trying again... [{imei_try+1}/{MAX_RETRIES}]")
time.sleep(1)
imei_try += 1
return imei1, imei2
def _resolve_serial(self, params):
serial = params.get("HardwareSerial", encoding='utf8') or HARDWARE.get_serial()
def _resolve_serial(self):
serial = self.params.get("HardwareSerial", encoding='utf8') or HARDWARE.get_serial()
return serial
def register_device(self, spinner=None, timeout=60, verbose=False):
self.spinner = spinner
params = Params()
sunnylink_dongle_id, comma_dongle_id = self._resolve_dongle_ids(params)
sunnylink_dongle_id, comma_dongle_id = self._resolve_dongle_ids()
if comma_dongle_id is None:
self._status_update("Comma dongle ID not found, deferring sunnylink's registration to comma's registration process.")
return None
imei1, imei2 = self._resolve_imeis(params)
serial = self._resolve_serial(params)
imei1, imei2 = self._resolve_imeis()
serial = self._resolve_serial()
if sunnylink_dongle_id not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID):
return sunnylink_dongle_id
@@ -96,7 +102,7 @@ class SunnylinkApi(BaseApi):
raise Exception(f"Failed to register with sunnylink. Status code: {resp.status_code}")
else:
dongleauth = json.loads(resp.text)
sunnylink_dongle_id = dongleauth["dongle_id"]
sunnylink_dongle_id = dongleauth["device_id"]
if sunnylink_dongle_id:
self._status_update("Device registered successfully.")
break
@@ -112,7 +118,7 @@ class SunnylinkApi(BaseApi):
break
if sunnylink_dongle_id:
params.put("SunnylinkDongleId", sunnylink_dongle_id)
self.params.put("SunnylinkDongleId", sunnylink_dongle_id)
self.spinner = None
return sunnylink_dongle_id
+9 -3
View File
@@ -127,8 +127,8 @@ std::unordered_map<std::string, uint32_t> keys = {
{"GitCommit", PERSISTENT},
{"GitCommitDate", PERSISTENT},
{"GitDiff", PERSISTENT},
{"GithubSshKeys", PERSISTENT},
{"GithubUsername", PERSISTENT},
{"GithubSshKeys", PERSISTENT | BACKUP},
{"GithubUsername", PERSISTENT | BACKUP},
{"GitRemote", PERSISTENT},
{"GsmApn", PERSISTENT | BACKUP},
{"GsmMetered", PERSISTENT | BACKUP},
@@ -191,7 +191,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"RecordFrontLock", PERSISTENT}, // for the internal fleet
{"ReplayControlsState", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"SshEnabled", PERSISTENT},
{"SshEnabled", PERSISTENT | BACKUP},
{"TermsVersion", PERSISTENT},
{"Timezone", PERSISTENT},
{"TrainingVersion", PERSISTENT},
@@ -263,6 +263,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"HotspotOnBootConfirmed", PERSISTENT},
{"LastCarModel", PERSISTENT | BACKUP},
{"LastSpeedLimitSignTap", PERSISTENT},
{"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START},
{"LiveTorque", PERSISTENT | BACKUP},
{"LiveTorqueRelaxed", PERSISTENT | BACKUP},
{"LkasToggle", PERSISTENT | BACKUP},
@@ -306,6 +307,8 @@ std::unordered_map<std::string, uint32_t> keys = {
{"StockLongToyota", PERSISTENT | BACKUP},
{"SubaruManualParkingBrakeSng", PERSISTENT | BACKUP},
{"SunnylinkDongleId", PERSISTENT},
{"SunnylinkEnabled", PERSISTENT},
{"SunnylinkdPid", PERSISTENT},
{"TorqueDeadzoneDeg", PERSISTENT | BACKUP},
{"TorqueFriction", PERSISTENT | BACKUP},
{"TorqueMaxLatAccel", PERSISTENT | BACKUP},
@@ -321,6 +324,9 @@ std::unordered_map<std::string, uint32_t> keys = {
{"VwCCOnly", PERSISTENT | BACKUP},
{"Offroad_SupersededUpdate", PERSISTENT},
{"SunnylinkCache_Users", PERSISTENT},
{"SunnylinkCache_Roles", PERSISTENT},
// PFEIFER - MAPD {{
{"MapdVersion", PERSISTENT},
{"RoadName", CLEAR_ON_ONROAD_TRANSITION},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 130 B

+1 -1
Submodule panda updated: 87391d0067...3b7e3d5885
+7
View File
@@ -70,7 +70,9 @@ system/version.py
selfdrive/athena/__init__.py
selfdrive/athena/athenad.py
selfdrive/athena/manage_athenad.py
selfdrive/athena/manage_sunnylinkd.py
selfdrive/athena/registration.py
selfdrive/athena/sunnylinkd.py
selfdrive/boardd/.gitignore
selfdrive/boardd/SConscript
@@ -298,6 +300,11 @@ selfdrive/ui/qt/*.cc
selfdrive/ui/qt/*.h
selfdrive/ui/qt/network/*.cc
selfdrive/ui/qt/network/*.h
selfdrive/ui/qt/network/sunnylink/*.cc
selfdrive/ui/qt/network/sunnylink/*.h
selfdrive/ui/qt/network/sunnylink/models/*.h
selfdrive/ui/qt/network/sunnylink/services/*.cc
selfdrive/ui/qt/network/sunnylink/services/*.h
selfdrive/ui/qt/offroad/*.cc
selfdrive/ui/qt/offroad/*.h
selfdrive/ui/qt/offroad/*.qml
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.
Binary file not shown.
+10 -5
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
from __future__ import annotations
import platform
import base64
import bz2
import hashlib
@@ -751,11 +752,15 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None:
onroad_prev = onroad
if sock is not None:
# While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s
# offroad, we can expect to time out in 30 + (10 * 3) = 60s
# FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30)
if platform.system() == 'Darwin': # macOS
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 7 if onroad else 30)
else:
# While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s
# offroad, we can expect to time out in 30 + (10 * 3) = 60s
# FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2 if onroad else 3)
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
#TODO: Add this to files_common to allow release to public
import time
from multiprocessing import Process
from openpilot.common.params import Params
from openpilot.selfdrive.manager.process import launcher
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE
from openpilot.system.version import get_version, get_normalized_origin, get_short_branch, get_commit, is_dirty
SUNNYLINK_MGR_PID_PARAM = "SunnylinkdPid"
def main():
params = Params()
dongle_id = params.get("SunnylinkDongleId").decode('utf-8')
cloudlog.bind_global(dongle_id=dongle_id,
version=get_version(),
origin=get_normalized_origin(),
branch=get_short_branch(),
commit=get_commit(),
dirty=is_dirty(),
device=HARDWARE.get_device_type())
try:
while 1:
cloudlog.info("starting athena daemon")
proc = Process(name='sunnylinkd', target=launcher, args=('selfdrive.athena.sunnylinkd', 'sunnylinkd'))
proc.start()
proc.join()
cloudlog.event("sunnylinkd exited", exitcode=proc.exitcode)
time.sleep(5)
except Exception:
cloudlog.exception("manage_sunnylinkd.exception")
finally:
params.remove(SUNNYLINK_MGR_PID_PARAM)
if __name__ == '__main__':
main()
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
#TODO: Add this to files_common to allow release to public
from __future__ import annotations
import os
import threading
import time
from openpilot.selfdrive.athena.athenad import ws_send, jsonrpc_handler, \
recv_queue, RECONNECT_TIMEOUT_S, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage
from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException,
create_connection)
from openpilot.common.api import SunnylinkApi
from openpilot.common.params import Params
from openpilot.common.realtime import set_core_affinity
from openpilot.common.swaglog import cloudlog
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai')
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
LOCAL_PORT_WHITELIST = {8022}
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
end_event = threading.Event()
threads = [
threading.Thread(target=ws_manage, args=(ws, end_event), name='ws_manage'),
threading.Thread(target=ws_recv, args=(ws, end_event), name='ws_recv'),
threading.Thread(target=ws_send, args=(ws, end_event), name='ws_send'),
threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'),
# threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'),
# threading.Thread(target=log_handler, args=(end_event,), name='log_handler'),
# threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'),
] + [
threading.Thread(target=jsonrpc_handler, args=(end_event,), name=f'worker_{x}')
for x in range(HANDLER_THREADS)
]
for thread in threads:
thread.start()
try:
while not end_event.wait(0.1):
if exit_event is not None and exit_event.is_set():
end_event.set()
except (KeyboardInterrupt, SystemExit):
end_event.set()
raise
finally:
for thread in threads:
cloudlog.debug(f"athena.joining {thread.name}")
thread.join()
def ws_recv(ws: WebSocket, end_event: threading.Event) -> None:
last_ping = int(time.monotonic() * 1e9)
while not end_event.is_set():
try:
opcode, data = ws.recv_data(control_frame=True)
if opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
if opcode == ABNF.OPCODE_TEXT:
data = data.decode("utf-8")
recv_queue.put_nowait(data)
elif opcode in (ABNF.OPCODE_PING, ABNF.OPCODE_PONG):
last_ping = int(time.monotonic() * 1e9)
Params().put("LastSunnylinkPingTime", str(last_ping))
except WebSocketTimeoutException:
ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping
if ns_since_last_ping > RECONNECT_TIMEOUT_S * 1e9:
cloudlog.exception("sunnylinkd.ws_recv.timeout")
end_event.set()
except Exception:
cloudlog.exception("sunnylinkd.ws_recv.exception")
end_event.set()
def ws_ping(ws: WebSocket, end_event: threading.Event) -> None:
# last_ping = int(time.monotonic() * 1e9)
while not end_event.is_set():
try:
ws.ping()
except Exception:
cloudlog.exception("sunnylinkd.ws_ping.exception")
end_event.set()
time.sleep(RECONNECT_TIMEOUT_S * 0.8) # Sleep about 80% before a timeout
def main(exit_event: threading.Event = None):
try:
set_core_affinity([0, 1, 2, 3])
except Exception:
cloudlog.exception("failed to set core affinity")
params = Params()
dongle_id = params.get("SunnylinkDongleId", encoding='utf-8')
UploadQueueCache.initialize(upload_queue)
ws_uri = SUNNYLINK_ATHENA_HOST
api = SunnylinkApi(dongle_id)
conn_start = None
conn_retries = 0
while exit_event is None or not exit_event.is_set():
try:
if conn_start is None:
conn_start = time.monotonic()
cloudlog.event("sunnylinkd.main.connecting_ws", ws_uri=ws_uri, retries=conn_retries)
ws = create_connection(ws_uri,
cookie="jwt=" + api.get_token(),
enable_multithread=True,
timeout=30.0)
cloudlog.event("sunnylinkd.main.connected_ws", ws_uri=ws_uri, retries=conn_retries,
duration=time.monotonic() - conn_start)
conn_start = None
conn_retries = 0
cur_upload_items.clear()
handle_long_poll(ws, exit_event)
except (KeyboardInterrupt, SystemExit):
break
except (ConnectionError, TimeoutError, WebSocketException):
conn_retries += 1
params.remove("LastSunnylinkPingTime")
except Exception:
cloudlog.exception("sunnylinkd.main.exception")
conn_retries += 1
params.remove("LastSunnylinkPingTime")
time.sleep(backoff(conn_retries))
if __name__ == "__main__":
main()
+1
View File
@@ -227,6 +227,7 @@ class CarInterface(CarInterfaceBase):
# 0x2AA is sent by a similar device which intercepts the radar instead of DSU on NO_DSU_CARs
if 0x2FF in fingerprint[0] or (0x2AA in fingerprint[0] and candidate in NO_DSU_CAR):
ret.flags |= ToyotaFlags.SMART_DSU.value
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_SDSU
# No radar dbc for cars without DSU which are not TSS 2.0
# TODO: make an adas dbc file for dsu-less models
+2 -1
View File
@@ -70,6 +70,7 @@ def manager_init() -> None:
("LastSpeedLimitSignTap", "0"),
("LkasToggle", "0"),
("MadsIconToggle", "1"),
("MapdVersion", f"{VERSION}"),
("MaxTimeOffroad", "9"),
("NNFF", "0"),
("OnroadScreenOff", "-2"),
@@ -100,7 +101,7 @@ def manager_init() -> None:
("OsmDbUpdatesCheck", "0"),
("OsmDownloadedDate", "0"),
("OSMDownloadProgress", "{}"),
("MapdVersion", f"{VERSION}"),
("SunnylinkEnabled", "1"),
]
if not PC:
default_params.append(("LastUpdateTime", datetime.datetime.utcnow().isoformat().encode('utf8')))
+5
View File
@@ -98,6 +98,11 @@ procs = [
PythonProcess("webjoystick", "tools.bodyteleop.web", notcar),
]
if os.path.exists("../athena/manage_sunnylinkd.py") and Params().get_bool("SunnylinkEnabled"):
procs += [
DaemonProcess("manage_sunnylinkd", "selfdrive.athena.manage_sunnylinkd", "SunnylinkdPid"),
]
if os.path.exists("./gitlab_runner.sh") and True: # Of course and True is always true. Placeholder for a param :D
# Only devs!
procs += [
+10 -1
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
from openpilot.common.api.sunnylink import SunnylinkApi
from openpilot.common.params import Params
from openpilot.common.spinner import Spinner
from openpilot.system.version import is_prebuilt
@@ -7,11 +9,18 @@ from openpilot.system.version import is_prebuilt
if __name__ == "__main__":
spinner = Spinner()
extra_args = {}
if not Params().get_bool("SunnylinkEnabled"):
print("Sunnylink is not enabled. Exiting.")
spinner.close()
exit(0)
if not is_prebuilt():
extra_args = {
"verbose": True,
"timeout": 60
}
SunnylinkApi(None).register_device(spinner, **extra_args)
sunnylink_id = SunnylinkApi(None).register_device(spinner, **extra_args)
print(f"SunnyLinkId: {sunnylink_id}")
spinner.close()
+4 -1
View File
@@ -37,6 +37,9 @@ widgets_src += ["qt/offroad/sunnypilot/display_settings.cc", "qt/offroad/sunnypi
"qt/offroad/sunnypilot/speed_limit_warning_settings.cc", "qt/offroad/sunnypilot/speed_limit_policy_settings.cc",
"qt/offroad/sunnypilot/sunnylink_settings.cc"]
widgets_src += ["qt/network/sunnylink/sunnylink_client.cc", "qt/network/sunnylink/services/base_device_service.cc",
"qt/network/sunnylink/services/role_service.cc", "qt/network/sunnylink/services/user_service.cc"]
qt_env['CPPDEFINES'] = []
if maps:
base_libs += ['QMapLibre']
@@ -103,7 +106,7 @@ if GetOption('extras'):
qt_env.Program('tests/ui_snapshot', [asset_obj, "tests/ui_snapshot.cc"] + qt_src, LIBS=qt_libs)
if GetOption('extras') and arch != "Darwin":
if GetOption('extras') and arch in ['larch64']:
# setup and factory resetter
qt_env.Program("qt/setup/reset", ["qt/setup/reset.cc"], LIBS=qt_libs)
qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj],
+3 -1
View File
@@ -17,6 +17,7 @@
#include <string>
#include "common/params.h"
#include "common/swaglog.h"
#include "common/util.h"
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h"
@@ -201,6 +202,7 @@ bool HttpRequest::timeout() const {
}
void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method, const QByteArray &payload) {
LOGD("Requesting %s", qPrintable(requestURL));
if (active()) {
qDebug() << "HttpRequest is active";
return;
@@ -216,7 +218,7 @@ void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Meth
QNetworkRequest request;
request.setUrl(QUrl(requestURL));
request.setRawHeader("User-Agent", getUserAgent().toUtf8());
request.setRawHeader("User-Agent", getUserAgent(sunnylink).toUtf8());
if (!payload.isEmpty()) {
request.setRawHeader("Content-Type", "application/json");
}
+1 -1
View File
@@ -165,7 +165,7 @@ void MapWindow::updateState(const UIState &s) {
// set path color on change, and show map on rising edge of navigate on openpilot
auto car_control = sm["carControl"].getCarControl();
bool nav_enabled = sm["modelV2"].getModelV2().getNavEnabled() &&
(car_control.getLatActive() || car_control.getLongActive());
(sm["controlsState"].getControlsState().getEnabled() || car_control.getLatActive() || car_control.getLongActive());
if (nav_enabled != uiState()->scene.navigate_on_openpilot) {
if (loaded_once) {
m_map->setPaintProperty("navLayer", "line-color", getNavPathColor(nav_enabled));
+31 -6
View File
@@ -26,17 +26,29 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) {
wifiScreen = new QWidget(this);
QVBoxLayout* vlayout = new QVBoxLayout(wifiScreen);
vlayout->setContentsMargins(20, 20, 20, 20);
QHBoxLayout* hlayout = new QHBoxLayout();
QPushButton* scanButton = new QPushButton(tr("Scan"));
scanButton->setObjectName("scan_btn");
scanButton->setFixedSize(400, 100);
connect(wifi, &WifiManager::refreshSignal, this, [=]() { scanButton->setText(tr("Scan")); scanButton->setEnabled(true); });
connect(scanButton, &QPushButton::clicked, [=]() { scanButton->setText(tr("Scanning...")); scanButton->setEnabled(false); wifi->requestScan(); });
hlayout->addWidget(scanButton);
hlayout->addStretch(1); // Pushes the button all the way to the left
if (show_advanced) {
hlayout->setSpacing(10);
QPushButton* advancedSettings = new QPushButton(tr("Advanced"));
advancedSettings->setObjectName("advanced_btn");
advancedSettings->setStyleSheet("margin-right: 30px;");
advancedSettings->setFixedSize(400, 100);
connect(advancedSettings, &QPushButton::clicked, [=]() { main_layout->setCurrentWidget(an); });
vlayout->addSpacing(10);
vlayout->addWidget(advancedSettings, 0, Qt::AlignRight);
vlayout->addSpacing(10);
hlayout->addWidget(advancedSettings);
}
vlayout->addLayout(hlayout);
vlayout->addSpacing(10);
wifiWidget = new WifiUI(this, wifi);
wifiWidget->setObjectName("wifiWidget");
connect(wifiWidget, &WifiUI::connectToNetwork, this, &Networking::connectToNetwork);
@@ -57,7 +69,7 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) {
setPalette(pal);
setStyleSheet(R"(
#wifiWidget > QPushButton, #back_btn, #advanced_btn {
#wifiWidget > QPushButton, #back_btn, #advanced_btn, #scan_btn{
font-size: 50px;
margin: 0px;
padding: 15px;
@@ -66,7 +78,7 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) {
color: #dddddd;
background-color: #393939;
}
#back_btn:pressed, #advanced_btn:pressed {
#back_btn:pressed, #advanced_btn:pressed, #scan_btn:pressed {
background-color: #4a4a4a;
}
)");
@@ -214,6 +226,19 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid
});
list->addItem(hiddenNetworkButton);
// Ngrok
QProcess process;
process.start("sudo service ngrok status | grep running");
process.waitForFinished();
QString output = QString(process.readAllStandardOutput());
bool ngrokRunning = !output.isEmpty();
ToggleControl *ngrokToggle = new ToggleControl(tr("Ngrok Service"), "", "", ngrokRunning);
connect(ngrokToggle, &ToggleControl::toggleFlipped, [=](bool state) {
if (state) std::system("sudo ngrok service start");
else std::system("sudo ngrok service stop");
});
list->addItem(ngrokToggle);
// Set initial config
wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn")), metered);
@@ -0,0 +1,60 @@
#ifndef ROLE_MODEL_H
#define ROLE_MODEL_H
#include <QJsonObject>
enum class RoleType {
ReadOnly,
Sponsor,
Admin
};
// haha, a role model xD
class RoleModel {
protected:
QJsonObject m_raw_json_object;
public:
RoleType roleType;
explicit RoleModel(const RoleType &roleType) : roleType(roleType) {
m_raw_json_object = toJson();
}
explicit RoleModel(const QJsonObject &json) : RoleModel(stringToRoleType(json["role_type"].toString())) {
m_raw_json_object = json;
}
[[nodiscard]] QJsonObject toJson() const {
QJsonObject json;
json["role_type"] = roleTypeToString(roleType);
return json;
}
static RoleType stringToRoleType(const QString &roleTypeString) {
if (roleTypeString == "ReadOnly") {
return RoleType::ReadOnly;
} else if (roleTypeString == "Sponsor") {
return RoleType::Sponsor;
} else { // Default to Admin
return RoleType::Admin;
}
}
static QString roleTypeToString(const RoleType &roleType) {
switch (roleType) {
case RoleType::ReadOnly:
return "ReadOnly";
case RoleType::Sponsor:
return "Sponsor";
default: // RoleType::Admin
return "Admin";
}
}
template <typename T, typename = typename std::enable_if<std::is_base_of<RoleModel, T>::value>::type>
T as() const {
return T(m_raw_json_object);
}
};
#endif
@@ -0,0 +1,84 @@
#ifndef SPONSORROLE_MODEL_H
#define SPONSORROLE_MODEL_H
#include <QJsonObject>
enum class SponsorTier {
Free,
Novice,
Supporter,
Contributor,
Benefactor,
Guardian,
};
// haha, a role model xD
class SponsorRoleModel final : RoleModel {
public:
SponsorTier roleTier;
explicit SponsorRoleModel(const RoleType &roleType, const SponsorTier &roleTier) : RoleModel(roleType), roleTier(roleTier) {}
explicit SponsorRoleModel(const QJsonObject &json) : RoleModel(json), roleTier(stringToSponsorTier(json["role_tier"].toString())) {}
[[nodiscard]] QJsonObject toJson() const {
QJsonObject json = RoleModel::toJson();
json["role_tier"] = sponsorTierToString(roleTier);
return json;
};
static SponsorTier stringToSponsorTier(const QString &sponsorTierString) {
const auto sponsorTierStringLower = sponsorTierString.toLower();
if (sponsorTierStringLower == "guardian")
return SponsorTier::Guardian;
if (sponsorTierStringLower == "novice")
return SponsorTier::Novice;
if (sponsorTierStringLower == "supporter")
return SponsorTier::Supporter;
if (sponsorTierStringLower == "contributor")
return SponsorTier::Contributor;
if (sponsorTierStringLower == "benefactor")
return SponsorTier::Benefactor;
// Default to Guardian
return SponsorTier::Free;
}
static QString sponsorTierToString(const SponsorTier &sponsorTier) {
switch (sponsorTier) {
case SponsorTier::Guardian:
return "Guardian";
case SponsorTier::Novice:
return "Novice";
case SponsorTier::Supporter:
return "Supporter";
case SponsorTier::Contributor:
return "Contributor";
case SponsorTier::Benefactor:
return "Benefactor";
default: // SponsorTier::Free
return "Free";
}
}
[[nodiscard]] auto getSponsorTierString() const { return sponsorTierToString(roleTier); }
static QString sponsorTierToColor(const SponsorTier &sponsorTier) {
switch (sponsorTier) {
case SponsorTier::Guardian:
return "gold";
case SponsorTier::Benefactor:
return "mediumseagreen";
case SponsorTier::Contributor:
return "steelblue";
case SponsorTier::Supporter:
return "mediumpurple";
case SponsorTier::Novice:
return "white";
default: // SponsorTier::Free
return "silver";
}
}
[[nodiscard]] auto getSponsorTierColor() const { return sponsorTierToColor(roleTier); }
};
#endif
@@ -0,0 +1,33 @@
#ifndef USER_MODEL_H
#define USER_MODEL_H
#include <QJsonObject>
class UserModel {
public:
QString device_id;
QString user_id;
qint64 created_at;
qint64 updated_at;
QString token_hash;
explicit UserModel(const QJsonObject &json) {
device_id = json["device_id"].toString();
user_id = json["user_id"].toString();
created_at = json["created_at"].toInt();
updated_at = json["updated_at"].toInt();
token_hash = json["token_hash"].toString();
}
[[nodiscard]] QJsonObject toJson() const {
QJsonObject json;
json["device_id"] = device_id;
json["user_id"] = user_id;
json["created_at"] = created_at;
json["updated_at"] = updated_at;
json["token_hash"] = token_hash;
return json;
}
};
#endif
@@ -0,0 +1,48 @@
#include "base_device_service.h"
#include "common/swaglog.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/offroad/sunnypilot/sunnylink_settings.h"
BaseDeviceService::BaseDeviceService(QObject* parent) : QObject(parent), initial_request(nullptr), repeater(nullptr) {
param_watcher = new ParamWatcher(this);
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString &param_name, const QString &param_value) {
paramsRefresh();
});
param_watcher->addParam("SunnylinkEnabled");
}
void BaseDeviceService::paramsRefresh() {
}
void BaseDeviceService::loadDeviceData(const QString &url, bool poll) {
if (!is_sunnylink_enabled()) {
LOGW("Sunnylink is not enabled, refusing to load data.");
return;
}
auto sl_dongle_id = getSunnylinkDongleId();
if (!sl_dongle_id.has_value())
return;
QString fullUrl = SUNNYLINK_BASE_URL + "/device/" + *sl_dongle_id + url;
if (poll && !isCurrentyPolling()) {
LOGD("Polling %s", qPrintable(fullUrl));
LOGD("Cache key: SunnylinkCache_%s", qPrintable(QString(getCacheKey())));
repeater = new RequestRepeater(this, fullUrl, "SunnylinkCache_" + getCacheKey(), 60, false, true);
connect(repeater, &RequestRepeater::requestDone, this, &BaseDeviceService::handleResponse);
} else if(isCurrentyPolling()){
repeater->ForceUpdate();
} else {
LOGD("Sending one-time %s", qPrintable(fullUrl));
initial_request = new HttpRequest(this, true, 10000, true);
connect(initial_request, &HttpRequest::requestDone, this, &BaseDeviceService::handleResponse);
}
}
void BaseDeviceService::stopPolling() {
if (repeater != nullptr) {
repeater->deleteLater();
repeater = nullptr;
}
}
@@ -0,0 +1,29 @@
#ifndef BASESERVICE_H
#define BASESERVICE_H
#include "selfdrive/ui/qt/api.h"
#include "selfdrive/ui/qt/request_repeater.h"
#include "selfdrive/ui/qt/util.h"
class BaseDeviceService : public QObject {
Q_OBJECT
protected:
void paramsRefresh();
void loadDeviceData(const QString &url, bool poll = false);
virtual void handleResponse(const QString &response, bool success) = 0;
static bool is_sunnylink_enabled() { return Params().getBool("SunnylinkEnabled");};
ParamWatcher* param_watcher;
HttpRequest* initial_request = nullptr;
RequestRepeater* repeater = nullptr;
public:
explicit BaseDeviceService(QObject* parent = nullptr);
virtual QString getCacheKey() const = 0;
bool isCurrentyPolling() {return repeater != nullptr;}
void stopPolling();
};
#endif
@@ -0,0 +1,29 @@
#include "selfdrive/ui/qt/network/sunnylink/services/role_service.h"
#include <QJsonArray>
#include <QJsonDocument>
RoleService::RoleService(QObject* parent) : BaseDeviceService(parent) {}
void RoleService::load() {
loadDeviceData(url);
}
void RoleService::startPolling() {
loadDeviceData(url, true);
}
void RoleService::handleResponse(const QString &response, bool success) {
if (!success) return;
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
QJsonArray jsonArray = doc.array();
std::vector<RoleModel> roles;
for (const auto &value : jsonArray) {
roles.emplace_back(value.toObject());
}
emit rolesReady(roles);
uiState()->setSunnylinkRoles(roles);
}
@@ -0,0 +1,26 @@
#ifndef ROLESERVICE_H
#define ROLESERVICE_H
#include "selfdrive/ui/qt/network/sunnylink/services/base_device_service.h"
#include "selfdrive/ui/qt/network/sunnylink/models/role_model.h"
class RoleService : public BaseDeviceService {
Q_OBJECT
public:
explicit RoleService(QObject* parent = nullptr);
void load();
void startPolling();
[[nodiscard]] QString getCacheKey() const final { return "Roles"; };
signals:
void rolesReady(const std::vector<RoleModel> &roles);
protected:
void handleResponse(const QString&response, bool success) override;
private:
QString url = "/roles";
};
#endif
@@ -0,0 +1,31 @@
#include "selfdrive/ui/qt/network/sunnylink/services/user_service.h"
#include <QJsonArray>
#include <QJsonDocument>
UserService::UserService(QObject* parent) : BaseDeviceService(parent) {
url = "/users";
}
void UserService::load() {
loadDeviceData(url);
}
void UserService::startPolling() {
loadDeviceData(url, true);
}
void UserService::handleResponse(const QString &response, bool success) {
if (!success) return;
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
QJsonArray jsonArray = doc.array();
std::vector<UserModel> users;
for (const auto &value : jsonArray) {
users.emplace_back(value.toObject());
}
emit usersReady(users);
uiState()->setSunnylinkDeviceUsers(users);
}
@@ -0,0 +1,26 @@
#ifndef USERSERVICE_H
#define USERSERVICE_H
#include "selfdrive/ui/qt/network/sunnylink/services/base_device_service.h"
#include "selfdrive/ui/qt/network/sunnylink/models/user_model.h"
class UserService : public BaseDeviceService {
Q_OBJECT
public:
explicit UserService(QObject* parent = nullptr);
void load();
void startPolling();
[[nodiscard]] QString getCacheKey() const final { return "Users"; };
signals:
void usersReady(const std::vector<UserModel>&users);
protected:
void handleResponse(const QString&response, bool success) override;
private:
QString url = "/users";
};
#endif
@@ -0,0 +1,7 @@
#include "selfdrive/ui/qt/network/sunnylink/sunnylink_client.h"
#include "selfdrive/ui/qt/network/sunnylink/services/user_service.h"
SunnylinkClient::SunnylinkClient(QObject* parent) : QObject(parent) {
role_service = new RoleService(parent);
user_service = new UserService(parent);
}
@@ -0,0 +1,18 @@
#ifndef SUNNYLINK_CLIENT_H
#define SUNNYLINK_CLIENT_H
#include <QObject>
#include "selfdrive/ui/qt/network/sunnylink/services/role_service.h"
#include "selfdrive/ui/qt/network/sunnylink/services/user_service.h"
class SunnylinkClient : public QObject {
Q_OBJECT
public:
explicit SunnylinkClient(QObject* parent);
RoleService* role_service;
UserService* user_service;
};
#endif
+12 -4
View File
@@ -321,11 +321,19 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
});
addItem(resetCalibBtn);
auto resetMapboxTokenBtn = new ButtonControl(tr("Reset Mapbox Access Token"), tr("RESET"), "");
auto resetMapboxTokenBtn = new ButtonControl(tr("Reset Access Tokens for Map Services"), tr("RESET"), tr("Reset self-service access tokens for Mapbox, Amap, and Google Maps."));
connect(resetMapboxTokenBtn, &ButtonControl::clicked, [=]() {
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset the Mapbox access token?"), tr("Reset"), this)) {
params.remove("CustomMapboxTokenPk");
params.remove("CustomMapboxTokenSk");
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset access tokens for all map services?"), tr("Reset"), this)) {
std::vector<std::string> tokens = {
"CustomMapboxTokenPk",
"CustomMapboxTokenSk",
"AmapKey1",
"AmapKey2",
"GmapKey"
};
for (const auto& token : tokens) {
params.remove(token);
}
}
});
addItem(resetMapboxTokenBtn);
@@ -1,4 +1,5 @@
#include "selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.h"
#include <QThread>
ModelsFetcher::ModelsFetcher(QObject* parent) : QObject(parent) {
manager = new QNetworkAccessManager(this);
@@ -83,9 +84,30 @@ void ModelsFetcher::onFinished(QNetworkReply* reply, const QString& destinationP
QString finalPath = QDir(destinationPath).filePath(finalFilename);
// Save the downloaded file
QFile file(finalPath);
if (!file.open(QIODevice::WriteOnly)) {
return; // Consider emitting a signal or logging an error here as well
//ensure if the path exists and if not create it
if(!QDir().mkpath(destinationPath))
{
LOGE("Unable to create directory: %s", destinationPath.toStdString().c_str());
emit downloadFailed(filename);
return; // Stop further processing
}
//Retry the file open and write 3 times with a little delay between each retry
for (int i = 0; i < 3; i++) {
if (file.isOpen()) break;
file.open(QIODevice::WriteOnly);
if (!file.isOpen()) QThread::msleep(100);
}
// If the file is still not open, log an error and emit a failure signal
if (!file.isOpen()) {
LOGE("Unable to open file for writing: %s", finalPath.toStdString().c_str());
emit downloadFailed(filename);
return; // Stop further processing
}
file.write(data);
@@ -18,19 +18,41 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) {
handleDownloadProgress(progress, "metadata");
});
connect(&models_fetcher, &ModelsFetcher::downloadComplete, this, [this](const QByteArray&data, bool fromCache = false) {
connect(&models_fetcher, &ModelsFetcher::downloadComplete, this, [this](const QByteArray& data, bool fromCache) {
modelFromCache = fromCache;
updateLabels();
if (!isDownloadingModel() && modelDownloadProgress.has_value()) {
params.put("DrivingModelText", selectedModelToDownload->fullName.toStdString());
params.put("DrivingModelName", selectedModelToDownload->displayName.toStdString());
selectedModelToDownload.reset();
modelDownloadProgress.reset();
params.putBool("CustomDrivingModel", !model_download_failed);
}
nav_models_fetcher.download(selectedNavModelToDownload->downloadUriNav, selectedNavModelToDownload->fileNameNav);
HandleModelDownloadProgressReport();
});
connect(&nav_models_fetcher, &ModelsFetcher::downloadComplete, this, [this](const QByteArray&data, bool fromCache = false) {
navModelFromCache = fromCache;
updateLabels();
if (!isDownloadingNavModel() && navModelDownloadProgress.has_value()) {
params.put("DrivingModelGeneration", selectedNavModelToDownload->generation.toStdString());
params.put("NavModelText", selectedNavModelToDownload->fullNameNav.toStdString());
selectedNavModelToDownload.reset();
navModelDownloadProgress.reset();
}
metadata_fetcher.download(selectedMetadataToDownload->downloadUriMetadata, selectedMetadataToDownload->fileNameMetadata);
HandleModelDownloadProgressReport();
// updateLabels();
});
connect(&metadata_fetcher, &ModelsFetcher::downloadComplete, this, [this](const QByteArray&data, bool fromCache = false) {
metadataFromCache = fromCache;
updateLabels();
if (!isDownloadingMetadata() && metadataDownloadProgress.has_value()) {
params.put("DrivingModelMetadataText", selectedMetadataToDownload->fullNameMetadata.toStdString());
selectedMetadataToDownload.reset();
metadataDownloadProgress.reset();
}
HandleModelDownloadProgressReport();
// updateLabels();
});
connect(&models_fetcher, &ModelsFetcher::downloadFailed, this, &SoftwarePanelSP::handleDownloadFailed);
@@ -107,38 +129,32 @@ void SoftwarePanelSP::HandleModelDownloadProgressReport() {
// Driving model status
if (isDownloadingModel()) {
description += QString(tr("Downloading Driving model") + " [%1]... (%2%)")
.arg(drivingModelName)
.arg(QString::number(modelDownloadProgress.value_or(0.0), 'f', 2));
.arg(drivingModelName, QString::number(modelDownloadProgress.value_or(0.0), 'f', 2));
} else {
if (modelFromCache) drivingModelName += QString(" " + tr("(CACHED)"));
description += QString(tr("Driving model") + " [%1] " + tr("downloaded")
.arg(drivingModelName));
description += QString(tr("Driving model") + " [%1] " + tr("downloaded")).arg(drivingModelName);
}
// Navigation model status
if (isDownloadingNavModel()) {
if (!description.isEmpty()) description += "\n"; // Add newline if driving model status is already appended
description += QString(tr("Downloading Navigation model") + " [%1]... (%2%)")
.arg(navModelName)
.arg(QString::number(navModelDownloadProgress.value_or(0.0), 'f', 2));
.arg(navModelName, QString::number(navModelDownloadProgress.value_or(0.0), 'f', 2));
} else {
if (navModelFromCache) navModelName += QString(" " + tr("(CACHED)"));
if (!description.isEmpty()) description += "\n"; // Ensure newline separation
description += QString(tr("Navigation model") + " [%1] " + tr("downloaded")
.arg(navModelName));
description += QString(tr("Navigation model") + " [%1] " + tr("downloaded")).arg(navModelName);
}
// Metadata status
if (isDownloadingMetadata()) {
if (!description.isEmpty()) description += "\n";
description += QString(tr("Downloading Metadata model") + " [%1]... (%2%)")
.arg(metadataName)
.arg(QString::number(metadataDownloadProgress.value_or(0.0), 'f', 2));
.arg(metadataName, QString::number(metadataDownloadProgress.value_or(0.0), 'f', 2));
} else {
if (metadataFromCache) metadataName += QString(" " + tr("(CACHED)"));
if (!description.isEmpty()) description += "\n";
description += QString(tr("Metadata model") + " [%1] " + tr("downloaded")
.arg(metadataName));
description += QString(tr("Metadata model") + " [%1] " + tr("downloaded")).arg(metadataName);
}
if (model_download_failed) {
@@ -151,37 +167,6 @@ void SoftwarePanelSP::HandleModelDownloadProgressReport() {
currentModelLblBtn->showDescription();
currentModelLblBtn->setEnabled(
!(is_onroad || (isDownloadingModel() || isDownloadingMetadata() || isDownloadingNavModel())));
// If not downloading and there is a selected model, update parameters
if (!isDownloadingModel() && modelDownloadProgress.has_value()) {
params.put("DrivingModelText", selectedModelToDownload->fullName.toStdString());
params.put("DrivingModelName", selectedModelToDownload->displayName.toStdString());
//params.put("DrivingModelUrl", selectedModelToDownload->downloadUri.toStdString()); // TODO: Placeholder for future implementation
LOGD("Resetting selectedModelToDownload");
selectedModelToDownload.reset();
modelDownloadProgress.reset();
modelFromCache = false;
params.putBool("CustomDrivingModel", !model_download_failed);
}
// If not downloading and there is a selected model, update parameters
if (!isDownloadingNavModel() && navModelDownloadProgress.has_value()) {
params.put("DrivingModelGeneration", selectedNavModelToDownload->generation.toStdString());
params.put("NavModelText", selectedNavModelToDownload->fullNameNav.toStdString());
LOGD("Resetting selectedNavModelToDownload");
selectedNavModelToDownload.reset();
navModelDownloadProgress.reset();
navModelFromCache = false;
}
if (!isDownloadingMetadata() && metadataDownloadProgress.has_value()) {
params.put("DrivingModelMetadataText", selectedMetadataToDownload->fullNameMetadata.toStdString());
LOGD("Resetting selectedMetadataToDownload");
selectedMetadataToDownload.reset();
metadataDownloadProgress.reset();
metadataFromCache = false;
}
}
void SoftwarePanelSP::handleCurrentModelLblBtnClicked() {
@@ -240,14 +225,24 @@ void SoftwarePanelSP::handleCurrentModelLblBtnClicked() {
model_download_failed = false;
currentModelLblBtn->setValue(selectedModelToDownload->displayName);
currentModelLblBtn->setDescription(selectedModelToDownload->displayName);
// So we reset the cache status
modelFromCache = false;
navModelFromCache = false;
metadataFromCache = false;
// So we can signal them as pending
navModelDownloadProgress = 0.01;
modelDownloadProgress = 0.01;
metadataDownloadProgress = 0.01;
//Start the download, we download the other models on emit of downloadComplete
if(params.get("DrivingModelGeneration") != selectedModelToDownload->generation.toStdString())
showResetParamsDialog();
models_fetcher.download(selectedModelToDownload->downloadUri, selectedModelToDownload->fileName);
nav_models_fetcher.download(selectedNavModelToDownload->downloadUriNav, selectedNavModelToDownload->fileNameNav);
metadata_fetcher.download(selectedMetadataToDownload->downloadUriMetadata,
selectedMetadataToDownload->fileNameMetadata);
// Disable select button until download completes
currentModelLblBtn->setEnabled(false);
showResetParamsDialog();
}
updateLabels();
}
@@ -1,20 +1,76 @@
#include "selfdrive/ui/qt/offroad/sunnypilot/sunnylink_settings.h"
#include <common/watchdog.h>
#include <QtConcurrent/QtConcurrent>
SunnylinkPanel::SunnylinkPanel(QWidget* parent) : QFrame(parent) {
main_layout = new QStackedLayout(this);
sunnylink_client = new SunnylinkClient(this);
param_watcher = new ParamWatcher(this);
param_watcher->addParam("SunnylinkEnabled");
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString &param_name, const QString &param_value) {
paramsRefresh(param_name, param_value);
});
ListWidget *list = new ListWidget(this, false);
list->addItem(new LabelControl(tr("sunnylink Dongle ID"), getSunnylinkDongleId().value_or(tr("N/A"))));
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
connect(uiState(), &UIState::sunnylinkRolesChanged, this, &SunnylinkPanel::updateLabels);
connect(uiState(), &UIState::sunnylinkDeviceUsersChanged, this, &SunnylinkPanel::updateLabels);
auto list = new ListWidget(this, false);
sunnylinkEnabledBtn = new ParamControl(
"SunnylinkEnabled",
tr("Enable sunnylink"),
sunnylinkBtnDescription,
"../assets/offroad/icon_blank.png"
);
sunnylinkEnabledBtn->setValue(tr("Device ID ")+ getSunnylinkDongleId().value_or(tr("N/A")));
list->addItem(sunnylinkEnabledBtn);
list->addItem(horizontal_line());
popup = new SunnylinkSponsorPopup(this);
sunnylinkBtnDescription = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that.");
connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() {
//resets the description to the default one for the easter egg
sunnylinkEnabledBtn->setDescription(sunnylinkBtnDescription);
});
connect(sunnylinkEnabledBtn, &ParamControl::toggleFlipped, [=](bool enabled) {
if (enabled) {
auto proud_description = "<font color='SeaGreen'>"+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ "</font>";
sunnylinkEnabledBtn->showDescription();
sunnylinkEnabledBtn->setDescription(proud_description);
} else {
auto shame_description = "<font color='orange'>"+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ "</font>";
sunnylinkEnabledBtn->showDescription();
sunnylinkEnabledBtn->setDescription(shame_description);
}
auto dialog_text = tr("A reboot is required to") + " " + (enabled ? tr("start") : tr("stop")) +" "+ tr("all connections and processes from sunnylink.") + "<br/><small>"+ tr("If that's not a problem for you, you can ignore this.")+ "</small>";
if (ConfirmationDialog::confirm(dialog_text, tr("Reboot Now!"), this)) {
Hardware::reboot();
}
updateLabels();
});
status_popup = new SunnylinkSponsorPopup(false, this);
sponsorBtn = new ButtonControl(
tr("Sponsor Status"), tr("SPONSOR"),
tr("Become a sponsor of sunnypilot to get early access to sunnylink features.")
tr("Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.")
);
list->addItem(sponsorBtn);
connect(sponsorBtn, &ButtonControl::clicked, [=]() {
popup->exec();
status_popup->exec();
});
list->addItem(horizontal_line());
pair_popup = new SunnylinkSponsorPopup(true, this);
pairSponsorBtn = new ButtonControl(
tr("Pair GitHub Account"), tr("PAIR"),
tr("Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.") + "🌟"
);
list->addItem(pairSponsorBtn);
connect(pairSponsorBtn, &ButtonControl::clicked, [=]() {
pair_popup->exec();
});
list->addItem(horizontal_line());
@@ -26,22 +82,15 @@ SunnylinkPanel::SunnylinkPanel(QWidget* parent) : QFrame(parent) {
backupSettings = new SubPanelButton(tr("Backup Settings"), 720, this);
backupSettings->setObjectName("backup_btn");
// Set margin on the outside of the button
QVBoxLayout* backupSettingsLayout = new QVBoxLayout;
auto backupSettingsLayout = new QVBoxLayout;
backupSettingsLayout->setContentsMargins(0, 0, 0, 30);
backupSettingsLayout->addWidget(backupSettings);
connect(backupSettings, &QPushButton::clicked, [=]() {
is_backup = true;
backup_settings->started();
if (uiState()->isSubscriber()) {
if (ConfirmationDialog::confirm(tr("Are you sure you want to backup sunnypilot settings?"), tr("Back Up"), this)) {
backup_settings->sendParams(backup_settings->backupParams());
} else {
backup_settings->finished();
}
if (ConfirmationDialog::confirm(tr("Are you sure you want to backup sunnypilot settings?"), tr("Back Up"), this)) {
backup_settings->sendParams(backup_settings->backupParams());
} else {
if (ConfirmationDialog::confirm(tr("Early alpha access only. Become a sponsor to get early access to sunnylink features."), tr("Become a Sponsor"), this)) {
popup->exec();
}
backup_settings->finished();
}
is_backup = false;
@@ -52,13 +101,13 @@ SunnylinkPanel::SunnylinkPanel(QWidget* parent) : QFrame(parent) {
restoreSettings = new SubPanelButton(tr("Restore Settings"), 720, this);
restoreSettings->setObjectName("restore_btn");
// Set margin on the outside of the button
QVBoxLayout* restoreSettingsLayout = new QVBoxLayout;
auto restoreSettingsLayout = new QVBoxLayout;
restoreSettingsLayout->setContentsMargins(0, 0, 0, 30);
restoreSettingsLayout->addWidget(restoreSettings);
connect(restoreSettings, &QPushButton::clicked, [=]() {
is_restore = true;
backup_settings->started();
if (uiState()->isSubscriber()) {
if (uiState()->isSunnylinkSponsor()) {
if (ConfirmationDialog::confirm(tr("Are you sure you want to restore the last backed up sunnypilot settings?"), tr("Restore"), this)) {
backup_settings->getParams();
} else {
@@ -66,7 +115,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget* parent) : QFrame(parent) {
}
} else {
if (ConfirmationDialog::confirm(tr("Early alpha access only. Become a sponsor to get early access to sunnylink features."), tr("Become a Sponsor"), this)) {
popup->exec();
status_popup->exec();
}
backup_settings->finished();
}
@@ -75,7 +124,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget* parent) : QFrame(parent) {
connect(backup_settings, &BackupSettings::updateLabels, this, &SunnylinkPanel::updateLabels);
// Settings Restore and Settings Backup in the same horizontal space
QHBoxLayout *settings_layout = new QHBoxLayout;
auto settings_layout = new QHBoxLayout;
settings_layout->setContentsMargins(0, 0, 0, 30);
settings_layout->addWidget(backupSettings);
settings_layout->addSpacing(10);
@@ -89,24 +138,52 @@ SunnylinkPanel::SunnylinkPanel(QWidget* parent) : QFrame(parent) {
});
sunnylinkScreen = new QWidget(this);
QVBoxLayout* vlayout = new QVBoxLayout(sunnylinkScreen);
auto vlayout = new QVBoxLayout(sunnylinkScreen);
vlayout->setContentsMargins(50, 20, 50, 20);
vlayout->addWidget(new ScrollView(list, this), 1);
main_layout->addWidget(sunnylinkScreen);
getSubscriber();
if (is_sunnylink_enabled) {
startSunnylink();
}
updateLabels();
}
void SunnylinkPanel::showEvent(QShowEvent* event) {
getSubscriber(true);
if (is_sunnylink_enabled) {
startSunnylink();
}
updateLabels(); // For snappier feeling
}
void SunnylinkPanel::hideEvent(QHideEvent* event) {
sub_repeater->deleteLater();
void SunnylinkPanel::paramsRefresh(const QString &param_name, const QString &param_value) {
// We do it on paramsRefresh because the toggleEvent happens before the value is updated
if (param_name == "SunnylinkEnabled" && param_value == "1") {
startSunnylink();
} else if (param_name == "SunnylinkEnabled" && param_value == "0") {
stopSunnylink();
}
updateLabels();
}
void SunnylinkPanel::startSunnylink() const {
if (!sunnylink_client->role_service->isCurrentyPolling()) {
sunnylink_client->role_service->startPolling();
} else {
sunnylink_client->role_service->load();
}
if (!sunnylink_client->user_service->isCurrentyPolling()) {
sunnylink_client->user_service->startPolling();
} else {
sunnylink_client->user_service->load();
}
}
void SunnylinkPanel::stopSunnylink() const {
sunnylink_client->role_service->stopPolling();
sunnylink_client->user_service->stopPolling();
}
void SunnylinkPanel::updateLabels() {
@@ -114,67 +191,40 @@ void SunnylinkPanel::updateLabels() {
return;
}
bool is_sub = uiState()->isSubscriber();
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A"));
bool is_sub = uiState()->isSunnylinkSponsor() && is_sunnylink_enabled;
auto max_current_sponsor_rule = uiState()->sunnylinkSponsorRole();
auto role_name = max_current_sponsor_rule.getSponsorTierString();
std::optional role_color = max_current_sponsor_rule.getSponsorTierColor();
bool is_paired = uiState()->isSunnylinkPaired();
auto paired_users = uiState()->sunnylinkDeviceUsers();
sponsorBtn->setEnabled(!is_onroad);
sunnylinkEnabledBtn->setEnabled(!is_onroad);
sunnylinkEnabledBtn->setValue(tr("Device ID ")+ sunnylinkDongleId);
sponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled);
sponsorBtn->setText(is_sub ? tr("THANKS") + " ❤️" : tr("SPONSOR"));
sponsorBtn->setValue(is_sub ? tr("Sponsor") : tr("Not Sponsor"));
sponsorBtn->setValue(is_sub ? tr(role_name.toStdString().c_str()) : tr("Not Sponsor"), role_color);
backupSettings->setEnabled(!backup_settings->in_progress && !is_onroad);
restoreSettings->setEnabled(!backup_settings->in_progress && !is_onroad);
pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled);
pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired"));
backupSettings->setEnabled(!backup_settings->in_progress && !is_onroad && is_sunnylink_enabled);
restoreSettings->setEnabled(!backup_settings->in_progress && !is_onroad && is_sunnylink_enabled);
backupSettings->setText(backup_settings->in_progress && is_backup ? tr("Backing up...") : tr("Backup Settings"));
restoreSettings->setText(backup_settings->in_progress && is_restore ? tr("Restoring...") : tr("Restore Settings"));
if (!is_sunnylink_enabled) {
sunnylinkEnabledBtn->setValue("");
sponsorBtn->setValue("");
pairSponsorBtn->setValue("");
}
update();
}
void SunnylinkPanel::getSubscriber(bool poll) {
if (auto sl_dongle_id = getSunnylinkDongleId()) {
QString url = "https://stg.api.sunnypilot.ai/device/" + *sl_dongle_id + "/roles";
init_sub_request = new HttpRequest(this, true, 10000, true);
QObject::connect(init_sub_request, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
replyFinished(resp, success);
updateLabels();
init_sub_request->deleteLater();
});
init_sub_request->sendRequest(url);
if (poll) {
sub_repeater = new RequestRepeater(this, url, "", 60, false, true);
QObject::connect(sub_repeater, &RequestRepeater::requestDone, [=](const QString &resp, bool success) {
replyFinished(resp, success);
updateLabels();
});
}
}
}
void SunnylinkPanel::replyFinished(const QString &response, bool success) {
if (!success) return;
SunnylinkRoleType role_type;
if (response == "[]") {
role_type = static_cast<SunnylinkRoleType>(int(SunnylinkRoleType::SL_UNKNOWN));
uiState()->setSunnylinkRoleType(role_type);
qDebug() << "JSON Parse failed on getting sponsor status";
return;
}
// TODO: Refactor to check additional role types with new mapping on the server side when implemented
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
QJsonArray jsonArray = doc.array();
for (const QJsonValue &value : jsonArray) {
QJsonObject json = value.toObject();
role_type = static_cast<SunnylinkRoleType>(int(sunnylinkRoleTypeValue(QString(json["role_type"].toString()))));
uiState()->setSunnylinkRoleType(role_type);
}
}
BackupSettings::BackupSettings(QWidget* parent) : QFrame(parent) {
}
@@ -207,17 +257,17 @@ QByteArray BackupSettings::backupParams(const bool encrypt) {
QByteArray processed_data = encrypt ? CommaApi::rsa_encrypt(compressedData) : compressedData;
// Encode the compressed QByteArray in Base64.
QString converted_params_processed_base64_data = QString(processed_data.toBase64());
auto converted_params_processed_base64_data = QString(processed_data.toBase64());
payload["is_encrypted"] = encrypt;
payload["config"] = converted_params_processed_base64_data;
// Create a sub-object for sunnypilot_version
QJsonObject version;
QStringList versioning = getVersion().split('.');
version["major"] = versioning.value(0, 0).toInt();
version["minor"] = versioning.value(1, 0).toInt();
version["patch"] = versioning.value(2, 0).toInt();
version["build"] = versioning.value(3, 0).toInt();
version["major"] = versioning.value(0, nullptr).toInt();
version["minor"] = versioning.value(1, nullptr).toInt();
version["patch"] = versioning.value(2, nullptr).toInt();
version["build"] = versioning.value(3, nullptr).toInt();
version["branch"] = QString::fromStdString(params.get("GitBranch"));
payload["sunnypilot_version"] = version;
@@ -229,9 +279,9 @@ QByteArray BackupSettings::backupParams(const bool encrypt) {
void BackupSettings::sendParams(const QByteArray &payload) {
if (auto sl_dongle_id = getSunnylinkDongleId()) {
QString url = "https://stg.api.sunnypilot.ai/backup/" + *sl_dongle_id;
HttpRequest *request = new HttpRequest(this, true, 10000, true);
QObject::connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
QString url = SUNNYLINK_BASE_URL + "/backup/" + *sl_dongle_id;
auto request = new HttpRequest(this, true, 10000, true);
connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
if (success && resp != "[]") {
ConfirmationDialog::alert(tr("Settings backed up for sunnylink Device ID:") + " " + *sl_dongle_id, this);
} else if (resp == "[]") {
@@ -240,7 +290,7 @@ void BackupSettings::sendParams(const QByteArray &payload) {
ConfirmationDialog::alert(tr("OOPS! We made a booboo.") + "\n" + tr("Please try again later."), this);
}
QObject::connect(request, &QObject::destroyed, this, &BackupSettings::finished);
connect(request, &QObject::destroyed, this, &BackupSettings::finished);
request->deleteLater();
});
@@ -297,9 +347,9 @@ void BackupSettings::restoreParams(const QString &resp) {
void BackupSettings::getParams() {
if (auto sl_dongle_id = getSunnylinkDongleId()) {
QString url = "https://stg.api.sunnypilot.ai/backup/" + *sl_dongle_id;
HttpRequest *request = new HttpRequest(this, true, 10000, true);
QObject::connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
QString url = SUNNYLINK_BASE_URL + "/backup/" + *sl_dongle_id;
auto request = new HttpRequest(this, true, 10000, true);
connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
bool restart_ui = false;
if (success && resp != "[]") {
restoreParams(resp);
@@ -311,7 +361,7 @@ void BackupSettings::getParams() {
ConfirmationDialog::alert(tr("OOPS! We made a booboo.") + "\n" + tr("Please try again later."), this);
}
QObject::connect(request, &QObject::destroyed, [=]() {
connect(request, &QObject::destroyed, [=]() {
finished();
if (restart_ui) {
@@ -340,7 +390,7 @@ void BackupSettings::finished() {
// Sponsor Upsell
using qrcodegen::QrCode;
SunnylinkSponsorQRWidget::SunnylinkSponsorQRWidget(QWidget* parent) : QWidget(parent) {
SunnylinkSponsorQRWidget::SunnylinkSponsorQRWidget(bool sponsor_pair, QWidget* parent) : QWidget(parent), sponsor_pair(sponsor_pair) {
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &SunnylinkSponsorQRWidget::refresh);
}
@@ -357,7 +407,17 @@ void SunnylinkSponsorQRWidget::hideEvent(QHideEvent *event) {
}
void SunnylinkSponsorQRWidget::refresh() {
QString qrString = "https://github.com/sponsors/sunnyhaibin";
QString qrString;
if (sponsor_pair) {
QString token = CommaApi::create_jwt({}, 3600, true);
auto sl_dongle_id = getSunnylinkDongleId();
QByteArray payload = QString("1|" + *sl_dongle_id + "|" + token).toUtf8().toBase64();
qrString = SUNNYLINK_BASE_URL + "/sso?state=" + payload;
} else {
qrString = "https://github.com/sponsors/sunnyhaibin";
}
this->updateQrCode(qrString);
update();
}
@@ -388,50 +448,70 @@ void SunnylinkSponsorQRWidget::paintEvent(QPaintEvent *e) {
p.drawPixmap(s.width(), s.height(), img);
}
SunnylinkSponsorPopup::SunnylinkSponsorPopup(QWidget *parent) : DialogBase(parent) {
QHBoxLayout *hlayout = new QHBoxLayout(this);
QStringList SunnylinkSponsorPopup::getInstructions(bool sponsor_pair) {
QStringList instructions;
if (sponsor_pair) {
instructions << tr("Scan the QR code to login to your GitHub account")
<< tr("Follow the prompts to complete the pairing process")
<< tr("Re-enter the \"sunnylink\" panel to verify sponsorship status")
<< tr("If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot");
} else {
instructions << tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page")
<< tr("Choose your sponsorship tier and confirm your support")
<< tr("Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status");
}
return instructions;
}
SunnylinkSponsorPopup::SunnylinkSponsorPopup(bool sponsor_pair, QWidget *parent) : DialogBase(parent), sponsor_pair(sponsor_pair) {
auto *hlayout = new QHBoxLayout(this);
auto sunnylink_client = new SunnylinkClient(this);
hlayout->setContentsMargins(0, 0, 0, 0);
hlayout->setSpacing(0);
setStyleSheet("SunnylinkSponsorPopup { background-color: #E0E0E0; }");
// text
QVBoxLayout *vlayout = new QVBoxLayout();
auto vlayout = new QVBoxLayout();
vlayout->setContentsMargins(85, 70, 50, 70);
vlayout->setSpacing(50);
hlayout->addLayout(vlayout, 1);
{
QPushButton *close = new QPushButton(QIcon(":/icons/close.svg"), "", this);
auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this);
close->setIconSize(QSize(80, 80));
close->setStyleSheet("border: none;");
vlayout->addWidget(close, 0, Qt::AlignLeft);
QObject::connect(close, &QPushButton::clicked, this, &QDialog::reject);
connect(close, &QPushButton::clicked, this, [=] {
sunnylink_client->role_service->load();
sunnylink_client->user_service->load();
QDialog::reject();
});
//vlayout->addSpacing(30);
QLabel *title = new QLabel(tr("Early Access: Become a sunnypilot Sponsor"), this);
const QString titleText = sponsor_pair ? tr("Pair your GitHub account") : tr("Early Access: Become a sunnypilot Sponsor");
const auto title = new QLabel(titleText, this);
title->setStyleSheet("font-size: 75px; color: black;");
title->setWordWrap(true);
vlayout->addWidget(title);
QLabel *instructions = new QLabel(QString(R"(
<ol type='1' style='margin-left: 15px;'>
<li style='margin-bottom: 50px;'>%1</li>
<li style='margin-bottom: 50px;'>%2</li>
<li style='margin-bottom: 50px;'>%3</li>
</ol>
)").arg(tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page"))
.arg(tr("Choose your sponsorship tier and confirm your support"))
.arg(tr("Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status")), this);
QStringList instructions = getInstructions(sponsor_pair);
QString instructionsHtml = "<ol type='1' style='margin-left: 15px;'>";
for (const auto & instruction : instructions) {
instructionsHtml += QString("<li style='margin-bottom: 50px;'>%1</li>").arg(instruction);
}
instructionsHtml += "</ol>";
const auto instructionsLabel = new QLabel(instructionsHtml, this);
instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;");
instructions->setWordWrap(true);
vlayout->addWidget(instructions);
instructionsLabel->setStyleSheet("font-size: 47px; font-weight: bold; color: black;");
instructionsLabel->setWordWrap(true);
vlayout->addWidget(instructionsLabel);
vlayout->addStretch();
}
// QR code
SunnylinkSponsorQRWidget *qr = new SunnylinkSponsorQRWidget(this);
auto *qr = new SunnylinkSponsorQRWidget(sponsor_pair ? true : false, this);
hlayout->addWidget(qr, 1);
}
@@ -1,26 +1,22 @@
#pragma once
#include <QApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QtCore/qjsonobject.h>
#include <QrCode.hpp>
#include "selfdrive/ui/qt/network/sunnylink/sunnylink_client.h"
#include "common/watchdog.h"
#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/api.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/request_repeater.h"
#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
const QString SUNNYLINK_BASE_URL = util::getenv("SUNNYLINK_API_HOST", "https://stg.api.sunnypilot.ai").c_str();
// sponsor QR code
class SunnylinkSponsorQRWidget : public QWidget {
Q_OBJECT
public:
explicit SunnylinkSponsorQRWidget(QWidget* parent = 0);
explicit SunnylinkSponsorQRWidget(bool sponsor_pair = false, QWidget* parent = 0);
void paintEvent(QPaintEvent*) override;
private:
@@ -30,6 +26,8 @@ private:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
bool sponsor_pair = false;
private slots:
void refresh();
};
@@ -40,7 +38,11 @@ class SunnylinkSponsorPopup : public DialogBase {
Q_OBJECT
public:
explicit SunnylinkSponsorPopup(QWidget* parent);
explicit SunnylinkSponsorPopup(bool sponsor_pair = false, QWidget* parent = 0);
private:
static QStringList getInstructions(bool sponsor_pair);
bool sponsor_pair = false;
};
class BackupSettings : public QFrame {
@@ -50,7 +52,7 @@ public:
explicit BackupSettings(QWidget *parent = nullptr);
void sendParams(const QByteArray &payload);
void getParams();
QByteArray backupParams(const bool encrypt = true);
QByteArray backupParams(bool encrypt = true);
void restoreParams(const QString &resp);
void started();
void finished();
@@ -69,29 +71,33 @@ class SunnylinkPanel : public QFrame {
public:
explicit SunnylinkPanel(QWidget *parent = nullptr);
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void paramsRefresh(const QString&param_name, const QString&param_value);
public slots:
void updateLabels();
private:
void getSubscriber(bool poll = false);
void replyFinished(const QString &response, bool success);
ParamControl* sunnylinkEnabledBtn;
QStackedLayout* main_layout = nullptr;
QWidget* sunnylinkScreen = nullptr;
ScrollView *scrollView = nullptr;
RequestRepeater* sub_repeater = nullptr;
HttpRequest* init_sub_request = nullptr;
LabelControl *sunnylinkId;
BackupSettings *backup_settings;
SubPanelButton *restoreSettings;
SubPanelButton *backupSettings;
SunnylinkSponsorPopup *popup;
SunnylinkSponsorPopup *status_popup;
SunnylinkSponsorPopup *pair_popup;
ButtonControl* sponsorBtn;
ButtonControl* pairSponsorBtn;
SunnylinkClient* sunnylink_client;
bool is_onroad = false;
bool is_backup = false;
bool is_restore = false;
bool is_sunnylink_enabled = false;
ParamWatcher *param_watcher;
QString sunnylinkBtnDescription;
void stopSunnylink() const;
void startSunnylink() const;
};
+17 -8
View File
@@ -1,15 +1,14 @@
#include "selfdrive/ui/qt/request_repeater.h"
#include "common/swaglog.h"
RequestRepeater::RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey,
int period, bool while_onroad, bool sunnylink) : HttpRequest(parent, true, 20000, sunnylink) {
int period, bool whileOnroad, bool sunnylink) : HttpRequest(parent, true, 20000, sunnylink) {
request_url = requestURL;
while_onroad = whileOnroad;
timer = new QTimer(this);
timer->setTimerType(Qt::VeryCoarseTimer);
QObject::connect(timer, &QTimer::timeout, [=]() {
if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) {
sendRequest(requestURL);
}
});
connect(timer, &QTimer::timeout, [=]() { this->timerTick(); });
timer->start(period * 1000);
if (!cacheKey.isEmpty()) {
@@ -17,11 +16,21 @@ RequestRepeater::RequestRepeater(QObject *parent, const QString &requestURL, con
if (!prevResp.isEmpty()) {
QTimer::singleShot(500, [=]() { emit requestDone(prevResp, true, QNetworkReply::NoError); });
}
QObject::connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
if (success && resp != prevResp) {
params.put(cacheKey.toStdString(), resp.toStdString());
prevResp = resp;
}
});
}
// Don't wait for the timer to fire to send the first request
ForceUpdate();
}
void RequestRepeater::timerTick() {
if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) {
LOGD("Sending request for %s", qPrintable(request_url));
sendRequest(request_url);
}
}
+11 -2
View File
@@ -1,15 +1,24 @@
#pragma once
#include "common/swaglog.h"
#include "common/util.h"
#include "selfdrive/ui/qt/api.h"
#include "selfdrive/ui/ui.h"
class RequestRepeater : public HttpRequest {
public:
RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool while_onroad=false, bool sunnylink = false);
private:
Params params;
QTimer *timer;
QString prevResp;
QString request_url;
bool while_onroad;
void timerTick();
public:
RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool whileOnroad=false, bool sunnylink = false);
void ForceUpdate() {
LOGD("Forcing update for %s", qPrintable(request_url));
timerTick();
}
};
+2 -2
View File
@@ -29,8 +29,8 @@ QString getBrand() {
return QObject::tr("sunnypilot");
}
QString getUserAgent() {
return "openpilot-" + getVersion();
QString getUserAgent(bool sunnylink) {
return (sunnylink ? "sunnypilot-" : "openpilot-") + getVersion();
}
std::optional<QString> getDongleId() {
+1 -1
View File
@@ -15,7 +15,7 @@
QString getVersion();
QString getBrand();
QString getUserAgent();
QString getUserAgent(bool sunnylink = false);
std::optional<QString> getDongleId();
std::optional<QString> getSunnylinkDongleId();
QMap<QString, QString> getSupportedLanguages();
+9 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
@@ -23,6 +24,10 @@ public:
explicit ElidedLabel(QWidget *parent = 0);
explicit ElidedLabel(const QString &text, QWidget *parent = 0);
void setColor(const QString &color) {
setStyleSheet("QLabel { color : " + color + "; }");
}
signals:
void clicked();
@@ -50,8 +55,11 @@ public:
title_label->setText(title);
}
void setValue(const QString &val) {
void setValue(const QString &val, std::optional<QString> color = std::nullopt) {
value->setText(val);
if (color.has_value()) {
value->setColor(color.value());
}
}
const QString getDescription() {
+8 -11
View File
@@ -11,6 +11,7 @@
#include "common/swaglog.h"
#include "common/util.h"
#include "common/watchdog.h"
#include "qt/network/sunnylink/models/role_model.h"
#include "system/hardware/hw.h"
#define BACKLIGHT_DT 0.05
@@ -431,18 +432,14 @@ void UIState::setPrimeType(PrimeType type) {
}
}
void UIState::setSunnylinkRoleType(SunnylinkRoleType type) {
if (type != role_type) {
bool prev_subscriber = isSubscriber();
void UIState::setSunnylinkRoles(const std::vector<RoleModel>& roles) {
sunnylinkRoles = roles;
emit sunnylinkRolesChanged(roles);
}
role_type = type;
emit sunnylinkRoleTypeChanged(role_type);
bool subscriber = isSubscriber();
if (prev_subscriber != subscriber) {
emit sunnylinkRoleChanged(subscriber);
}
}
void UIState::setSunnylinkDeviceUsers(const std::vector<UserModel>& users) {
sunnylinkUsers = users;
emit sunnylinkDeviceUsersChanged(users);
}
Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) {
+43 -26
View File
@@ -2,6 +2,7 @@
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <QObject>
@@ -15,6 +16,9 @@
#include "common/mat.h"
#include "common/params.h"
#include "common/timing.h"
#include "qt/network/sunnylink/models/role_model.h"
#include "qt/network/sunnylink/models/sponsor_role_model.h"
#include "qt/network/sunnylink/models/user_model.h"
#include "system/hardware/hw.h"
const int UI_BORDER_SIZE = 30;
@@ -135,27 +139,6 @@ enum PrimeType {
PURPLE = 5,
};
enum SunnylinkRoleType {
SL_UNKNOWN = -1,
SL_READ_ONLY = 0,
SL_SPONSOR = 1,
SL_ADMIN = 2, // Internal use only
};
inline const QMap<QString, int> sunnylinkRoleTypeConverted() {
QMap<QString, int> map;
map["Unknown"] = SunnylinkRoleType::SL_UNKNOWN;
map["ReadOnly"] = SunnylinkRoleType::SL_READ_ONLY;
map["Sponsor"] = SunnylinkRoleType::SL_SPONSOR;
map["Admin"] = SunnylinkRoleType::SL_ADMIN; // Internal use only
return map;
}
inline const int sunnylinkRoleTypeValue(const QString &roleType) {
static QMap<QString, int> map = sunnylinkRoleTypeConverted();
return map.value(roleType, SunnylinkRoleType::SL_UNKNOWN); // Default to UNKNOWN if not found
}
const QColor bg_colors [] = {
[STATUS_DISENGAGED] = QColor(0x17, 0x33, 0x49, 0xc8),
[STATUS_OVERRIDE] = QColor(0x91, 0x9b, 0x95, 0xf1),
@@ -294,9 +277,41 @@ public:
inline PrimeType primeType() const { return prime_type; }
inline bool hasPrime() const { return prime_type != PrimeType::UNKNOWN && prime_type != PrimeType::NONE; }
void setSunnylinkRoleType(SunnylinkRoleType type);
inline SunnylinkRoleType sunnylinkRoleType() const { return role_type; }
inline bool isSubscriber() const { return role_type != SunnylinkRoleType::SL_UNKNOWN && role_type != SunnylinkRoleType::SL_READ_ONLY; }
void setSunnylinkRoles(const std::vector<RoleModel> &roles);
void setSunnylinkDeviceUsers(const std::vector<UserModel> &users);
inline std::vector<RoleModel> sunnylinkDeviceRoles() const { return sunnylinkRoles; }
inline bool isSunnylinkAdmin() const {
return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) {
return role.roleType == RoleType::Admin;
});
}
inline bool isSunnylinkSponsor() const {
return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) {
return role.roleType == RoleType::Sponsor && role.as<SponsorRoleModel>().roleTier != SponsorTier::Free;
});
}
inline SponsorRoleModel sunnylinkSponsorRole() const {
std::optional<SponsorRoleModel> sponsorRoleWithHighestTier = std::nullopt;
for (const auto &role : sunnylinkRoles) {
if(role.roleType != RoleType::Sponsor)
continue;
if (auto sponsorRole = role.as<SponsorRoleModel>(); !sponsorRoleWithHighestTier.has_value() || sponsorRoleWithHighestTier->roleTier < sponsorRole.roleTier) {
sponsorRoleWithHighestTier = sponsorRole;
}
}
return sponsorRoleWithHighestTier.value_or(SponsorRoleModel(RoleType::Sponsor, SponsorTier::Free));
}
inline SponsorTier sunnylinkSponsorTier() const {
return sunnylinkSponsorRole().roleTier;
}
inline std::vector<UserModel> sunnylinkDeviceUsers() const { return sunnylinkUsers; }
inline bool isSunnylinkPaired() const {
return std::any_of(sunnylinkUsers.begin(), sunnylinkUsers.end(), [](const UserModel &user) {
return user.user_id.toLower() != "unregisteredsponsor" && user.user_id.toLower() != "temporarysponsor";
});
}
int fb_w = 0, fb_h = 0;
@@ -316,7 +331,8 @@ signals:
void primeTypeChanged(PrimeType prime_type);
void sunnylinkRoleChanged(bool subscriber);
void sunnylinkRoleTypeChanged(SunnylinkRoleType role_type);
void sunnylinkRolesChanged(std::vector<RoleModel> roles);
void sunnylinkDeviceUsersChanged(std::vector<UserModel> users);
private slots:
void update();
@@ -325,7 +341,8 @@ private:
QTimer *timer;
bool started_prev = false;
PrimeType prime_type = PrimeType::UNKNOWN;
SunnylinkRoleType role_type = SunnylinkRoleType::SL_UNKNOWN;
std::vector<RoleModel> sunnylinkRoles = {};
std::vector<UserModel> sunnylinkUsers = {};
bool last_mads_enabled = false;
bool mads_path_state = false;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 131 B