diff --git a/.gitattributes b/.gitattributes
index 912d2b3866..8781a7371f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -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
diff --git a/.idea/tools/External Tools.xml b/.idea/tools/External Tools.xml
index 75b33a6fd7..92f206447d 100644
--- a/.idea/tools/External Tools.xml
+++ b/.idea/tools/External Tools.xml
@@ -20,4 +20,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.lfsconfig b/.lfsconfig
index 42dfa2d944..5b63415cd8 100644
--- a/.lfsconfig
+++ b/.lfsconfig
@@ -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
diff --git a/.lfsconfig-comma b/.lfsconfig-comma
new file mode 100644
index 0000000000..42dfa2d944
--- /dev/null
+++ b/.lfsconfig-comma
@@ -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
diff --git a/.run/Build Debug.run.xml b/.run/Build Debug.run.xml
index a7b47c9011..70f7e13dd2 100644
--- a/.run/Build Debug.run.xml
+++ b/.run/Build Debug.run.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/CHANGELOGS.md b/CHANGELOGS.md
index 57a373c944..83d7da1c71 100644
--- a/CHANGELOGS.md
+++ b/CHANGELOGS.md
@@ -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
diff --git a/common/api/sunnylink.py b/common/api/sunnylink.py
index 9166760dc7..c2ac89d6dd 100644
--- a/common/api/sunnylink.py
+++ b/common/api/sunnylink.py
@@ -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
diff --git a/common/params.cc b/common/params.cc
index e06508a283..9f9a053041 100644
--- a/common/params.cc
+++ b/common/params.cc
@@ -127,8 +127,8 @@ std::unordered_map 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 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 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 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 keys = {
{"VwCCOnly", PERSISTENT | BACKUP},
{"Offroad_SupersededUpdate", PERSISTENT},
+ {"SunnylinkCache_Users", PERSISTENT},
+ {"SunnylinkCache_Roles", PERSISTENT},
+
// PFEIFER - MAPD {{
{"MapdVersion", PERSISTENT},
{"RoadName", CLEAR_ON_ONROAD_TRANSITION},
diff --git a/docs/sunnyhaibin0850_qrcode_paypal.me.png b/docs/sunnyhaibin0850_qrcode_paypal.me.png
index e187bb2ea2..57d6024e01 100644
Binary files a/docs/sunnyhaibin0850_qrcode_paypal.me.png and b/docs/sunnyhaibin0850_qrcode_paypal.me.png differ
diff --git a/panda b/panda
index 87391d0067..3b7e3d5885 160000
--- a/panda
+++ b/panda
@@ -1 +1 @@
-Subproject commit 87391d0067bef069302f6da9a2c1200b16124e6a
+Subproject commit 3b7e3d5885347acdaa92a5d16cce2e324acc13c4
diff --git a/release/files_common b/release/files_common
index b8bf8c709f..880cbce0f4 100644
--- a/release/files_common
+++ b/release/files_common
@@ -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
diff --git a/selfdrive/assets/img_hands_on_wheel.png b/selfdrive/assets/img_hands_on_wheel.png
index 0b06b4c620..2c7c50d17c 100644
Binary files a/selfdrive/assets/img_hands_on_wheel.png and b/selfdrive/assets/img_hands_on_wheel.png differ
diff --git a/selfdrive/assets/img_minus_arrow_down.png b/selfdrive/assets/img_minus_arrow_down.png
index b41bbe7542..250f640585 100644
Binary files a/selfdrive/assets/img_minus_arrow_down.png and b/selfdrive/assets/img_minus_arrow_down.png differ
diff --git a/selfdrive/assets/img_plus_arrow_up.png b/selfdrive/assets/img_plus_arrow_up.png
index 0f72f27a06..14c1529da3 100644
Binary files a/selfdrive/assets/img_plus_arrow_up.png and b/selfdrive/assets/img_plus_arrow_up.png differ
diff --git a/selfdrive/assets/img_turn_left_icon.png b/selfdrive/assets/img_turn_left_icon.png
index 1a5ef61d7b..3f5f3b7de1 100644
Binary files a/selfdrive/assets/img_turn_left_icon.png and b/selfdrive/assets/img_turn_left_icon.png differ
diff --git a/selfdrive/assets/img_turn_right_icon.png b/selfdrive/assets/img_turn_right_icon.png
index d53e78a4c7..b317d14430 100644
Binary files a/selfdrive/assets/img_turn_right_icon.png and b/selfdrive/assets/img_turn_right_icon.png differ
diff --git a/selfdrive/assets/img_world_icon.png b/selfdrive/assets/img_world_icon.png
index fcfc9d95d9..91152fc92a 100644
Binary files a/selfdrive/assets/img_world_icon.png and b/selfdrive/assets/img_world_icon.png differ
diff --git a/selfdrive/assets/offroad/icon_acc_change.png b/selfdrive/assets/offroad/icon_acc_change.png
index e1e80cd172..19144942c9 100644
Binary files a/selfdrive/assets/offroad/icon_acc_change.png and b/selfdrive/assets/offroad/icon_acc_change.png differ
diff --git a/selfdrive/assets/offroad/icon_blank.png b/selfdrive/assets/offroad/icon_blank.png
index a1d2ae57e3..d5f4fcc00f 100644
Binary files a/selfdrive/assets/offroad/icon_blank.png and b/selfdrive/assets/offroad/icon_blank.png differ
diff --git a/selfdrive/assets/offroad/icon_display.png b/selfdrive/assets/offroad/icon_display.png
index 809f1daa09..58cbc39820 100644
Binary files a/selfdrive/assets/offroad/icon_display.png and b/selfdrive/assets/offroad/icon_display.png differ
diff --git a/selfdrive/assets/offroad/icon_dynamic_gac.png b/selfdrive/assets/offroad/icon_dynamic_gac.png
index 973715dad1..7394478dc5 100644
Binary files a/selfdrive/assets/offroad/icon_dynamic_gac.png and b/selfdrive/assets/offroad/icon_dynamic_gac.png differ
diff --git a/selfdrive/assets/offroad/icon_mute.png b/selfdrive/assets/offroad/icon_mute.png
index 3e31a13787..1639f23683 100644
Binary files a/selfdrive/assets/offroad/icon_mute.png and b/selfdrive/assets/offroad/icon_mute.png differ
diff --git a/selfdrive/assets/offroad/icon_software.png b/selfdrive/assets/offroad/icon_software.png
index c098c7999c..70915e2906 100644
Binary files a/selfdrive/assets/offroad/icon_software.png and b/selfdrive/assets/offroad/icon_software.png differ
diff --git a/selfdrive/assets/offroad/icon_toggle.png b/selfdrive/assets/offroad/icon_toggle.png
index 5c774db6f4..51906798b9 100644
Binary files a/selfdrive/assets/offroad/icon_toggle.png and b/selfdrive/assets/offroad/icon_toggle.png differ
diff --git a/selfdrive/assets/offroad/icon_trips.png b/selfdrive/assets/offroad/icon_trips.png
index a042cf94da..95dec05c29 100644
Binary files a/selfdrive/assets/offroad/icon_trips.png and b/selfdrive/assets/offroad/icon_trips.png differ
diff --git a/selfdrive/assets/offroad/icon_vehicle.png b/selfdrive/assets/offroad/icon_vehicle.png
index 68ef2d0c0b..4c036d9602 100644
Binary files a/selfdrive/assets/offroad/icon_vehicle.png and b/selfdrive/assets/offroad/icon_vehicle.png differ
diff --git a/selfdrive/assets/offroad/icon_visuals.png b/selfdrive/assets/offroad/icon_visuals.png
index 8d066c90ba..26530af357 100644
Binary files a/selfdrive/assets/offroad/icon_visuals.png and b/selfdrive/assets/offroad/icon_visuals.png differ
diff --git a/selfdrive/assets/sounds/prompt_single_high.wav b/selfdrive/assets/sounds/prompt_single_high.wav
index 83fe791b54..202483d17f 100644
Binary files a/selfdrive/assets/sounds/prompt_single_high.wav and b/selfdrive/assets/sounds/prompt_single_high.wav differ
diff --git a/selfdrive/assets/sounds/prompt_single_low.wav b/selfdrive/assets/sounds/prompt_single_low.wav
index 1fb94ec455..925401ea27 100644
Binary files a/selfdrive/assets/sounds/prompt_single_low.wav and b/selfdrive/assets/sounds/prompt_single_low.wav differ
diff --git a/selfdrive/athena/athenad.py b/selfdrive/athena/athenad.py
index 9f901498b7..21c4d736a0 100755
--- a/selfdrive/athena/athenad.py
+++ b/selfdrive/athena/athenad.py
@@ -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)
diff --git a/selfdrive/athena/manage_sunnylinkd.py b/selfdrive/athena/manage_sunnylinkd.py
new file mode 100755
index 0000000000..788d9fdf5e
--- /dev/null
+++ b/selfdrive/athena/manage_sunnylinkd.py
@@ -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()
diff --git a/selfdrive/athena/sunnylinkd.py b/selfdrive/athena/sunnylinkd.py
new file mode 100755
index 0000000000..f08cf2797b
--- /dev/null
+++ b/selfdrive/athena/sunnylinkd.py
@@ -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()
diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py
index 6160c4bf2d..ba5bcb6585 100644
--- a/selfdrive/car/toyota/interface.py
+++ b/selfdrive/car/toyota/interface.py
@@ -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
diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py
index 944fb12789..4fc5a7412d 100755
--- a/selfdrive/manager/manager.py
+++ b/selfdrive/manager/manager.py
@@ -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')))
diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py
index cc9d9900bc..b85704442e 100644
--- a/selfdrive/manager/process_config.py
+++ b/selfdrive/manager/process_config.py
@@ -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 += [
diff --git a/selfdrive/manager/sunnylink.py b/selfdrive/manager/sunnylink.py
index 6b9b4a8318..b329132bc2 100755
--- a/selfdrive/manager/sunnylink.py
+++ b/selfdrive/manager/sunnylink.py
@@ -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()
diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript
index cb21bd055b..1293b51120 100644
--- a/selfdrive/ui/SConscript
+++ b/selfdrive/ui/SConscript
@@ -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],
diff --git a/selfdrive/ui/qt/api.cc b/selfdrive/ui/qt/api.cc
index bcde113215..f39e10a64f 100644
--- a/selfdrive/ui/qt/api.cc
+++ b/selfdrive/ui/qt/api.cc
@@ -17,6 +17,7 @@
#include
#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");
}
diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc
index 3d6e35f383..c00e38bc78 100644
--- a/selfdrive/ui/qt/maps/map.cc
+++ b/selfdrive/ui/qt/maps/map.cc
@@ -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));
diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc
index 1924cbf0fe..7934fa10da 100644
--- a/selfdrive/ui/qt/network/networking.cc
+++ b/selfdrive/ui/qt/network/networking.cc
@@ -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);
diff --git a/selfdrive/ui/qt/network/sunnylink/models/role_model.h b/selfdrive/ui/qt/network/sunnylink/models/role_model.h
new file mode 100644
index 0000000000..d7f3a98281
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/models/role_model.h
@@ -0,0 +1,60 @@
+#ifndef ROLE_MODEL_H
+#define ROLE_MODEL_H
+
+#include
+
+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 ::value>::type>
+ T as() const {
+ return T(m_raw_json_object);
+ }
+};
+
+#endif
diff --git a/selfdrive/ui/qt/network/sunnylink/models/sponsor_role_model.h b/selfdrive/ui/qt/network/sunnylink/models/sponsor_role_model.h
new file mode 100644
index 0000000000..64aad1ca46
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/models/sponsor_role_model.h
@@ -0,0 +1,84 @@
+#ifndef SPONSORROLE_MODEL_H
+#define SPONSORROLE_MODEL_H
+
+#include
+
+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
diff --git a/selfdrive/ui/qt/network/sunnylink/models/user_model.h b/selfdrive/ui/qt/network/sunnylink/models/user_model.h
new file mode 100644
index 0000000000..72baa3983d
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/models/user_model.h
@@ -0,0 +1,33 @@
+#ifndef USER_MODEL_H
+#define USER_MODEL_H
+
+#include
+
+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
diff --git a/selfdrive/ui/qt/network/sunnylink/services/base_device_service.cc b/selfdrive/ui/qt/network/sunnylink/services/base_device_service.cc
new file mode 100644
index 0000000000..2f2561a49a
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/services/base_device_service.cc
@@ -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 ¶m_name, const QString ¶m_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;
+ }
+}
diff --git a/selfdrive/ui/qt/network/sunnylink/services/base_device_service.h b/selfdrive/ui/qt/network/sunnylink/services/base_device_service.h
new file mode 100644
index 0000000000..cd7aa9d9b9
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/services/base_device_service.h
@@ -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
diff --git a/selfdrive/ui/qt/network/sunnylink/services/role_service.cc b/selfdrive/ui/qt/network/sunnylink/services/role_service.cc
new file mode 100644
index 0000000000..8bd0904421
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/services/role_service.cc
@@ -0,0 +1,29 @@
+#include "selfdrive/ui/qt/network/sunnylink/services/role_service.h"
+
+#include
+#include
+
+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 roles;
+ for (const auto &value : jsonArray) {
+ roles.emplace_back(value.toObject());
+ }
+
+ emit rolesReady(roles);
+ uiState()->setSunnylinkRoles(roles);
+}
diff --git a/selfdrive/ui/qt/network/sunnylink/services/role_service.h b/selfdrive/ui/qt/network/sunnylink/services/role_service.h
new file mode 100644
index 0000000000..bc40342982
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/services/role_service.h
@@ -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 &roles);
+
+protected:
+ void handleResponse(const QString&response, bool success) override;
+
+private:
+ QString url = "/roles";
+};
+
+#endif
diff --git a/selfdrive/ui/qt/network/sunnylink/services/user_service.cc b/selfdrive/ui/qt/network/sunnylink/services/user_service.cc
new file mode 100644
index 0000000000..2d52421d69
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/services/user_service.cc
@@ -0,0 +1,31 @@
+#include "selfdrive/ui/qt/network/sunnylink/services/user_service.h"
+
+#include
+#include
+
+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 users;
+ for (const auto &value : jsonArray) {
+ users.emplace_back(value.toObject());
+ }
+
+ emit usersReady(users);
+ uiState()->setSunnylinkDeviceUsers(users);
+}
diff --git a/selfdrive/ui/qt/network/sunnylink/services/user_service.h b/selfdrive/ui/qt/network/sunnylink/services/user_service.h
new file mode 100644
index 0000000000..e4af93fa29
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/services/user_service.h
@@ -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&users);
+
+protected:
+ void handleResponse(const QString&response, bool success) override;
+
+private:
+ QString url = "/users";
+};
+
+#endif
diff --git a/selfdrive/ui/qt/network/sunnylink/sunnylink_client.cc b/selfdrive/ui/qt/network/sunnylink/sunnylink_client.cc
new file mode 100644
index 0000000000..2d1613c0eb
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/sunnylink_client.cc
@@ -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);
+}
diff --git a/selfdrive/ui/qt/network/sunnylink/sunnylink_client.h b/selfdrive/ui/qt/network/sunnylink/sunnylink_client.h
new file mode 100644
index 0000000000..872d80ccf7
--- /dev/null
+++ b/selfdrive/ui/qt/network/sunnylink/sunnylink_client.h
@@ -0,0 +1,18 @@
+#ifndef SUNNYLINK_CLIENT_H
+#define SUNNYLINK_CLIENT_H
+
+#include
+
+#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
diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc
index 1e2535195c..d401102c5d 100644
--- a/selfdrive/ui/qt/offroad/settings.cc
+++ b/selfdrive/ui/qt/offroad/settings.cc
@@ -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 tokens = {
+ "CustomMapboxTokenPk",
+ "CustomMapboxTokenSk",
+ "AmapKey1",
+ "AmapKey2",
+ "GmapKey"
+ };
+ for (const auto& token : tokens) {
+ params.remove(token);
+ }
}
});
addItem(resetMapboxTokenBtn);
diff --git a/selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.cc b/selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.cc
index 4f2aaf724f..f45aa55e5c 100644
--- a/selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.cc
+++ b/selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.cc
@@ -1,4 +1,5 @@
#include "selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.h"
+#include
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);
diff --git a/selfdrive/ui/qt/offroad/sunnypilot/software_settings_sp.cc b/selfdrive/ui/qt/offroad/sunnypilot/software_settings_sp.cc
index f288170054..ec72e71a78 100644
--- a/selfdrive/ui/qt/offroad/sunnypilot/software_settings_sp.cc
+++ b/selfdrive/ui/qt/offroad/sunnypilot/software_settings_sp.cc
@@ -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();
}
diff --git a/selfdrive/ui/qt/offroad/sunnypilot/sunnylink_settings.cc b/selfdrive/ui/qt/offroad/sunnypilot/sunnylink_settings.cc
index 4e709fd72f..935ed3fc56 100644
--- a/selfdrive/ui/qt/offroad/sunnypilot/sunnylink_settings.cc
+++ b/selfdrive/ui/qt/offroad/sunnypilot/sunnylink_settings.cc
@@ -1,20 +1,76 @@
#include "selfdrive/ui/qt/offroad/sunnypilot/sunnylink_settings.h"
+#include
+#include
+
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 ¶m_name, const QString ¶m_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 = ""+ tr("๐Welcome back! We're excited to see you've enabled sunnylink again! ๐")+ "";
+ sunnylinkEnabledBtn->showDescription();
+ sunnylinkEnabledBtn->setDescription(proud_description);
+ } else {
+ auto shame_description = ""+ 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 ๐.")+ "";
+ 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.") + " "+ tr("If that's not a problem for you, you can ignore this.")+ "";
+ 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 ¶m_name, const QString ¶m_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(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(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"(
-
-
%1
-
%2
-
%3
-
- )").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 = "";
+ for (const auto & instruction : instructions) {
+ instructionsHtml += QString("