Merge branch 'master-priv' into disable-on-screen-recorder
@@ -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
|
||||
|
||||
@@ -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="-fno-inline"" />
|
||||
<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>
|
||||
@@ -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
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 130 B |
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 130 B |
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -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 += [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 ¶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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¶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 = "<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 ¶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<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¶m_name, const QString¶m_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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 131 B |