Merge remote-tracking branch 'origin/master-new' into HKG-long-tune

# Conflicts:
#	opendbc_repo
This commit is contained in:
discountchubbs
2025-03-20 06:09:21 -07:00
108 changed files with 3601 additions and 1191 deletions
+1 -1
View File
@@ -229,7 +229,7 @@ jobs:
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Test car models
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.routes-cache.outputs.cache-hit == 'true') && 1 || 20 }}
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.routes-cache.outputs.cache-hit == 'true') && 1 || 6 }}
run: |
${{ env.RUN }} "MAX_EXAMPLES=1 $PYTEST selfdrive/car/tests/test_models.py && \
chmod -R 777 /tmp/comma_download_cache"
@@ -21,8 +21,6 @@ on:
push:
branches: [ master, master-new, master-dev-c3-new ]
tags: [ '*' ]
pull_request:
branches: [ master, master-new ]
workflow_dispatch:
inputs:
extra_version:
+14 -1
View File
@@ -95,8 +95,21 @@ jobs:
for ((i=0; i<${#A[*]}; i=i+1));
do
# Check if the master file exists
if [ ! -f "${{ github.workspace }}/master_ui/${A[$i]}.png" ]; then
# This is a new file in PR UI that doesn't exist in master
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
DIFF="${DIFF}<table>"
if ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> master <img src=\"https://dummyimage.com/1986x993/000000/ffffff?text=++Not+in+master\"> </td>"
DIFF="${DIFF} <td> proposed <img src=\"https://raw.githubusercontent.com/sunnypilot/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
DIFF="${DIFF}</tr>"
DIFF="${DIFF}</table>"
DIFF="${DIFF}</details>"
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png
composite mask.png ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png
convert -delay 100 ${{ github.workspace }}/master_ui/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif
+1 -1
View File
@@ -6,7 +6,7 @@ Version 0.9.8 (2025-02-28)
========================
* New driving model
* Model now gates applying positive acceleration in Chill mode
* New driving monitoring model
* New driver monitoring model
* Reduced false positives related to passengers
* Image processing pipeline moved to the ISP
* More GPU time for bigger driving models
+2 -1
View File
@@ -136,7 +136,8 @@ struct OnroadEventSP @0xda96579883444c35 {
}
struct CarParamsSP @0x80ae746ee2596b11 {
flags @0 :UInt32; # flags for car specific quirks in sunnypilot
flags @0 :UInt32; # flags for car specific quirks in sunnypilot
safetyParam @1 : Int16; # flags for sunnypilot's custom safety flags
}
struct CarControlSP @0xa5cd762cd951a455 {
+3 -1
View File
@@ -59,6 +59,7 @@ struct OnroadEvent @0xc4fa6047f024e718 {
pcmEnable @23;
pcmDisable @24;
radarFault @25;
radarTempUnavailable @93;
brakeHold @26;
parkBrake @27;
manualRestart @28;
@@ -728,7 +729,7 @@ struct PeripheralState {
struct RadarState @0x9a185389d6fdd05f {
mdMonoTime @6 :UInt64;
carStateMonoTime @11 :UInt64;
radarErrors @12 :List(Car.RadarData.Error);
radarErrors @13 :Car.RadarData.Error;
leadOne @3 :LeadData;
leadTwo @4 :LeadData;
@@ -762,6 +763,7 @@ struct RadarState @0x9a185389d6fdd05f {
calPercDEPRECATED @9 :Int8;
canMonoTimesDEPRECATED @10 :List(UInt64);
cumLagMsDEPRECATED @5 :Float32;
radarErrorsDEPRECATED @12 :List(Car.RadarData.ErrorDEPRECATED);
}
struct LiveCalibrationData {
+4 -9
View File
@@ -1,13 +1,9 @@
from openpilot.common.api.comma_connect import CommaConnectApi
from sunnypilot.sunnylink.api import SunnylinkApi
class Api:
def __init__(self, dongle_id, use_sunnylink=False):
if use_sunnylink:
self.service = SunnylinkApi(dongle_id)
else:
self.service = CommaConnectApi(dongle_id)
def __init__(self, dongle_id):
self.service = CommaConnectApi(dongle_id)
def request(self, method, endpoint, **params):
return self.service.request(method, endpoint, **params)
@@ -22,6 +18,5 @@ class Api:
return self.service.get_token(expiry_hours)
def api_get(endpoint, method='GET', timeout=None, access_token=None, use_sunnylink=False, **params):
return SunnylinkApi(None).api_get(endpoint, method, timeout, access_token, **params) if use_sunnylink \
else CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params)
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params)
+3 -1
View File
@@ -132,7 +132,7 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// MADS params
{"Mads", PERSISTENT | BACKUP},
{"MadsMainCruiseAllowed", PERSISTENT | BACKUP},
{"MadsPauseLateralOnBrake", PERSISTENT | BACKUP},
{"MadsSteeringMode", PERSISTENT | BACKUP},
{"MadsUnifiedEngagementMode", PERSISTENT | BACKUP},
// Model Manager params
@@ -144,6 +144,8 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// sunnylink params
{"EnableSunnylinkUploader", PERSISTENT | BACKUP},
{"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START},
{"SunnylinkCache_Roles", PERSISTENT},
{"SunnylinkCache_Users", PERSISTENT},
{"SunnylinkDongleId", PERSISTENT},
{"SunnylinkdPid", PERSISTENT},
{"SunnylinkEnabled", PERSISTENT},
+8 -8
View File
@@ -34,25 +34,25 @@ function launch {
# Check to see if there's a valid overlay-based update available. Conditions
# are as follows:
#
# 1. The BASEDIR init file has to exist, with a newer modtime than anything in
# the BASEDIR Git repo. This checks for local development work or the user
# 1. The DIR init file has to exist, with a newer modtime than anything in
# the DIR Git repo. This checks for local development work or the user
# switching branches/forks, which should not be overwritten.
# 2. The FINALIZED consistent file has to exist, indicating there's an update
# that completed successfully and synced to disk.
if [ -f "${BASEDIR}/.overlay_init" ]; then
find ${BASEDIR}/.git -newer ${BASEDIR}/.overlay_init | grep -q '.' 2> /dev/null
if [ -f "${DIR}/.overlay_init" ]; then
find ${DIR}/.git -newer ${DIR}/.overlay_init | grep -q '.' 2> /dev/null
if [ $? -eq 0 ]; then
echo "${BASEDIR} has been modified, skipping overlay update installation"
echo "${DIR} has been modified, skipping overlay update installation"
else
if [ -f "${STAGING_ROOT}/finalized/.overlay_consistent" ]; then
if [ ! -d /data/safe_staging/old_openpilot ]; then
echo "Valid overlay update found, installing"
LAUNCHER_LOCATION="${BASH_SOURCE[0]}"
mv $BASEDIR /data/safe_staging/old_openpilot
mv "${STAGING_ROOT}/finalized" $BASEDIR
cd $BASEDIR
mv $DIR /data/safe_staging/old_openpilot
mv "${STAGING_ROOT}/finalized" $DIR
cd $DIR
echo "Restarting launch script ${LAUNCHER_LOCATION}"
unset AGNOS_VERSION
+1 -1
View File
@@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="11.10"
export AGNOS_VERSION="11.11"
fi
export STAGING_ROOT="/data/safe_staging"
+1 -1
Submodule panda updated: 5479e1572a...ccb9581b1c
+2
View File
@@ -99,11 +99,13 @@ dev = [
"av",
"azure-identity",
"azure-storage-blob",
"dbus-next",
"dictdiffer",
"lru-dict",
"matplotlib",
"parameterized >=0.8, <0.9",
"pyautogui",
"pygame",
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
"pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version
"pywinctl",
-2
View File
@@ -40,8 +40,6 @@ rm -f panda/board/obj/panda.bin.signed
rm -f panda/board/obj/panda_h7.bin.signed
VERSION=$(cat common/version.h | awk -F[\"-] '{print $2}')
echo "#define COMMA_VERSION \"$VERSION-release\"" > common/version.h
echo "[-] committing version $VERSION T=$SECONDS"
git add -f .
git commit -a -m "openpilot v$VERSION release"
+152 -118
View File
@@ -9,155 +9,189 @@ from datetime import datetime
def setup_argument_parser():
parser = argparse.ArgumentParser(description='Process and squash GitHub PRs')
parser.add_argument('--pr-data', type=str, help='PR data in JSON format')
parser.add_argument('--source-branch', type=str, default='master-new',
help='Source branch for merging')
parser.add_argument('--target-branch', type=str, default='master-dev-c3-new-test',
help='Target branch for merging')
parser.add_argument('--squash-script-path', type=str, required=True,
help='Path to the squash_and_merge.py script')
return parser
parser = argparse.ArgumentParser(description='Process and squash GitHub PRs')
parser.add_argument('--pr-data', type=str, help='PR data in JSON format')
parser.add_argument('--source-branch', type=str, default='master-new',
help='Source branch for merging')
parser.add_argument('--target-branch', type=str, default='master-dev-c3-new-test',
help='Target branch for merging')
parser.add_argument('--squash-script-path', type=str, required=True,
help='Path to the squash_and_merge.py script')
return parser
def validate_squash_script(script_path):
if not os.path.isfile(script_path):
raise FileNotFoundError(f"Squash script not found at: {script_path}")
if not os.access(script_path, os.X_OK):
raise PermissionError(f"Squash script is not executable: {script_path}")
if not os.path.isfile(script_path):
raise FileNotFoundError(f"Squash script not found at: {script_path}")
if not os.access(script_path, os.X_OK):
raise PermissionError(f"Squash script is not executable: {script_path}")
def sort_prs_by_creation(pr_data):
"""Sort PRs by creation date"""
nodes = (pr_data.get('data', {}).get('search', {}).get('nodes', []))
"""Sort PRs by creation date"""
nodes = (pr_data.get('data', {}).get('search', {}).get('nodes', []))
return sorted(
nodes,
key=lambda x: datetime.fromisoformat(x.get('createdAt', '').replace('Z', '+00:00'))
)
return sorted(
nodes,
key=lambda x: datetime.fromisoformat(x.get('createdAt', '').replace('Z', '+00:00'))
)
def add_pr_comment(pr_number, comment):
"""Add a comment to a PR using gh cli"""
try:
subprocess.run(
['gh', 'pr', 'comment', str(pr_number), '--body', comment],
check=True,
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
print(f"Failed to add comment to PR #{pr_number}: {e.stderr}")
"""Add or update a comment to a PR using gh cli"""
title = "## Squash and Merge"
try:
result = subprocess.run(
['gh', 'pr', 'view', str(pr_number), '--json', 'comments'],
check=True,
capture_output=True,
text=True
)
comments_data = json.loads(result.stdout)
has_existing_comment = False
for pr_comment in comments_data['comments']:
if pr_comment['body'].startswith(title):
has_existing_comment = True
break
full_comment = f"{title}\n\n{comment}"
if has_existing_comment:
subprocess.run(
['gh', 'pr', 'comment', '--edit-last', f"#{pr_number}", '--body', full_comment],
check=True,
capture_output=True,
text=True
)
else:
subprocess.run(
['gh', 'pr', 'comment', f"#{pr_number}", '--body', full_comment],
check=True,
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
print(f"Failed to add/update comment on PR #{pr_number}: {e.stderr}")
except json.JSONDecodeError:
print(f"Failed to parse comments data for PR #{pr_number}")
def validate_pr(pr):
"""Validate a PR and return (is_valid, skip_reason)"""
pr_number = pr.get('number', 'UNKNOWN')
branch = pr.get('headRefName', '')
"""Validate a PR and return (is_valid, skip_reason)"""
pr_number = pr.get('number', 'UNKNOWN')
branch = pr.get('headRefName', '')
if not branch:
return False, f"Missing branch name for PR #{pr_number}"
if not branch:
return False, f"missing branch name for PR #{pr_number}"
# Check if checks have passed
commits = pr.get('commits', {}).get('nodes', [])
if not commits:
return False, "No commit data found"
# Check if checks have passed
commits = pr.get('commits', {}).get('nodes', [])
if not commits:
return False, "no commit data found"
status = commits[0].get('commit', {}).get('statusCheckRollup', {})
if not status or status.get('state') != 'SUCCESS':
return False, "Not all checks have passed"
status = commits[0].get('commit', {}).get('statusCheckRollup', {})
if not status or status.get('state') != 'SUCCESS':
return False, "not all checks have passed"
# Check for merge conflicts
merge_status = subprocess.run(['gh', 'pr', 'view', str(pr_number), '--json', 'mergeable,mergeStateStatus'], capture_output=True, text=True)
merge_data = json.loads(merge_status.stdout)
if not merge_data.get('mergeable'):
return False, "Merge conflicts detected"
# Check for merge conflicts
merge_status = subprocess.run(['gh', 'pr', 'view', str(pr_number), '--json', 'mergeable,mergeStateStatus'],
capture_output=True, text=True)
merge_data = json.loads(merge_status.stdout)
if not merge_data.get('mergeable'):
return False, "merge conflicts detected"
if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND":
return False, f"Branch is `{mergeStateStatus}`"
if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND":
return False, f"branch is `{mergeStateStatus}`"
return True, None
return True, None
def process_pr(pr_data, source_branch, target_branch, squash_script_path):
try:
nodes = sort_prs_by_creation(pr_data)
if not nodes:
print("No PRs to squash")
return 0
try:
nodes = sort_prs_by_creation(pr_data)
if not nodes:
print("No PRs to squash")
return 0
print(f"Deleting target branch {target_branch}")
subprocess.run(['git', 'branch', '-D', target_branch], check=False)
subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True)
success_count = 0
for pr in nodes:
pr_number = pr.get('number', 'UNKNOWN')
branch = pr.get('headRefName', '')
title = pr.get('title', '')
is_valid, skip_reason = validate_pr(pr)
print(f"Deleting target branch {target_branch}")
subprocess.run(['git', 'branch', '-D', target_branch], check=False)
subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True)
success_count = 0
for pr in nodes:
pr_number = pr.get('number', 'UNKNOWN')
branch = pr.get('headRefName', '')
title = pr.get('title', '')
is_valid, skip_reason = validate_pr(pr)
if not is_valid:
print(f"Warning: {skip_reason} for PR #{pr_number}, skipping")
add_pr_comment(pr_number, f"⚠️ This PR was skipped in the automated `{target_branch}` squash because {skip_reason}.")
continue
if not is_valid:
print(f"Warning: {skip_reason} for PR #{pr_number}, skipping")
add_pr_comment(pr_number,
f"⚠️ This PR was skipped in the automated `{target_branch}` squash because **{skip_reason}**.")
continue
try:
# Fetch PR branch
subprocess.run(['git', 'fetch', 'origin', branch], check=True)
# Delete branch if it exists (ignore errors if it doesn't)
subprocess.run(['git', 'branch', '-D', branch], check=False)
# Create new branch pointing to origin's branch
subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True)
try:
# Fetch PR branch
subprocess.run(['git', 'fetch', 'origin', branch], check=True)
# Delete branch if it exists (ignore errors if it doesn't)
subprocess.run(['git', 'branch', '-D', branch], check=False)
# Create new branch pointing to origin's branch
subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True)
# Run squash script
subprocess.run([
squash_script_path,
'--target', target_branch,
'--source', branch,
'--title', f"{title} (#{pr_number})",
], check=True)
# Run squash script
subprocess.run([
squash_script_path,
'--target', target_branch,
'--source', branch,
'--title', f"{title} (#{pr_number})",
], check=True)
print(f"Successfully processed PR #{pr_number}")
success_count += 1
print(f"Successfully processed PR #{pr_number}")
success_count += 1
except subprocess.CalledProcessError as e:
print(f"Error processing PR #{pr_number}:")
print(f"Command failed with exit code {e.returncode}")
error_output = getattr(e, 'stderr', 'No error output available')
print(f"Error output: {error_output}")
add_pr_comment(pr_number, f"⚠️ Error during automated {target_branch} squash:\n```\n{error_output}\n```")
continue
except Exception as e:
print(f"Unexpected error processing PR #{pr_number}: {str(e)}")
continue
except subprocess.CalledProcessError as e:
print(f"Error processing PR #{pr_number}:")
print(f"Command failed with exit code {e.returncode}")
error_output = getattr(e, 'stderr', 'No error output available')
print(f"Error output: {error_output}")
add_pr_comment(pr_number,
f"⚠️ Error during automated `{target_branch}` squash:\n```\n{error_output}\n```")
continue
except Exception as e:
print(f"Unexpected error processing PR #{pr_number}: {str(e)}")
continue
return success_count
except Exception as e:
import traceback
print(f"Error in process_pr: {str(e)}")
print("Full traceback:")
print(traceback.format_exc())
return 0
def main():
parser = setup_argument_parser()
try:
args = parser.parse_args()
validate_squash_script(args.squash_script_path)
pr_data_json = json.loads(args.pr_data)
# Process the PRs
success_count = process_pr(pr_data_json, args.source_branch, args.target_branch, args.squash_script_path)
print(f"Successfully processed {success_count} PRs")
except Exception as e:
print(f"Fatal error: {str(e)}", file=sys.stderr)
return 1
return success_count
except Exception as e:
import traceback
print(f"Error in process_pr: {str(e)}")
print("Full traceback:")
print(traceback.format_exc())
return 0
def main():
parser = setup_argument_parser()
try:
args = parser.parse_args()
validate_squash_script(args.squash_script_path)
pr_data_json = json.loads(args.pr_data)
# Process the PRs
success_count = process_pr(pr_data_json, args.source_branch, args.target_branch, args.squash_script_path)
print(f"Successfully processed {success_count} PRs")
except Exception as e:
print(f"Fatal error: {str(e)}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
sys.exit(main())
+1
View File
@@ -8,6 +8,7 @@ ROOT = HERE + "/.."
blacklist = [
".git/",
".github/workflows/",
"matlab.*.md",
+10 -10
View File
@@ -16,7 +16,7 @@ from opendbc.car import DT_CTRL, structs
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
from opendbc.car.carlog import carlog
from opendbc.car.fw_versions import ObdCallback
from opendbc.car.car_helpers import get_car, get_radar_interface
from opendbc.car.car_helpers import get_car, interfaces
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
@@ -24,8 +24,8 @@ from openpilot.selfdrive.car.cruise import VCruiseHelper
from openpilot.selfdrive.car.car_specific import MockCarState
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
from openpilot.sunnypilot.mads.mads import MadsParams
from openpilot.sunnypilot.selfdrive.car import interfaces
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
REPLAY = "REPLAY" in os.environ
@@ -108,8 +108,8 @@ class Car:
fixed_fingerprint = json.loads(self.params.get("CarPlatformBundle", encoding='utf-8') or "{}").get("platform", None)
self.CI = get_car(*self.can_callbacks, obd_callback(self.params), experimental_long_allowed, num_pandas, cached_params, fixed_fingerprint)
interfaces.setup_car_interface_sp(self.CI.CP, self.CI.CP_SP, self.params)
self.RI = get_radar_interface(self.CI.CP, self.CI.CP_SP)
sunnypilot_interfaces.setup_car_interface_sp(self.CI.CP, self.CI.CP_SP, self.params)
self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP, self.CI.CP_SP)
self.CP = self.CI.CP
self.CP_SP = self.CI.CP_SP
@@ -126,8 +126,8 @@ class Car:
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
# mads
MadsParams().set_alternative_experience(self.CP)
MadsParams().set_car_specific_params(self.CP, self.CP_SP)
set_alternative_experience(self.CP, self.params)
set_car_specific_params(self.CP, self.CP_SP, self.params)
# Dynamic Experimental Control
self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl")
@@ -192,7 +192,7 @@ class Car:
self.rk = Ratekeeper(100, print_delay_threshold=None)
# log fingerprint in sentry
interfaces.log_fingerprint(self.CP)
sunnypilot_interfaces.log_fingerprint(self.CP)
def state_update(self) -> tuple[car.CarState, structs.RadarDataT | None]:
"""carState update loop, driven by can"""
@@ -256,7 +256,7 @@ class Car:
if RD is not None:
tracks_msg = messaging.new_message('liveTracks')
tracks_msg.valid = len(RD.errors) == 0
tracks_msg.valid = not any(RD.errors.to_dict().values())
tracks_msg.liveTracks = RD
self.pm.send('liveTracks', tracks_msg)
@@ -274,7 +274,7 @@ class Car:
# Initialize CarInterface, once controls are ready
# TODO: this can make us miss at least a few cycles when doing an ECU knockout
self.CI.init(self.CP, self.CP_SP, *self.can_callbacks)
interfaces.initialize_car_interface_sp(self.CP, self.CP_SP, self.params, *self.can_callbacks)
sunnypilot_interfaces.initialize_car_interface_sp(self.CP, self.CP_SP, self.params, *self.can_callbacks)
# signal pandad to switch to car safety mode
self.params.put_bool_nonblocking("ControlsReady", True)
+2 -2
View File
@@ -35,7 +35,7 @@ class TestCarInterfaces:
phases=(Phase.reuse, Phase.generate, Phase.shrink))
@given(data=st.data())
def test_car_interfaces(self, car_name, data):
CarInterface, CarController, CarState, RadarInterface = interfaces[car_name]
CarInterface = interfaces[car_name]
args = get_fuzzy_car_interface_args(data.draw)
@@ -44,7 +44,7 @@ class TestCarInterfaces:
car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'],
experimental_long=args['experimental_long'], docs=False)
car_params = car_params.as_reader()
car_interface = CarInterface(car_params, car_params_sp, CarController, CarState)
car_interface = CarInterface(car_params, car_params_sp)
assert car_params
assert car_params_sp
assert car_interface
+23 -22
View File
@@ -1,5 +1,4 @@
import time
import capnp
import copy
import os
import pytest
@@ -68,7 +67,7 @@ class TestCarModelBase(unittest.TestCase):
platform: Platform | None = None
test_route: CarTestRoute | None = None
can_msgs: list[capnp.lib.capnp._DynamicStructReader]
can_msgs: list[tuple[int, list[CanData]]]
fingerprint: dict[int, dict[int, int]]
elm_frame: int | None
car_safety_mode_frame: int | None
@@ -83,7 +82,8 @@ class TestCarModelBase(unittest.TestCase):
experimental_long = False
for msg in lr:
if msg.which() == "can":
can_msgs.append(msg)
can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0]
can_msgs.append((can[0], [CanData(*can) for can in can[1]]))
if len(can_msgs) <= FRAME_FINGERPRINT:
for m in msg.can:
if m.src < 64:
@@ -147,15 +147,13 @@ class TestCarModelBase(unittest.TestCase):
raise unittest.SkipTest
raise Exception(f"missing test route for {cls.platform}")
car_fw, can_msgs, experimental_long = cls.get_testing_data()
car_fw, cls.can_msgs, experimental_long = cls.get_testing_data()
# if relay is expected to be open in the route
cls.openpilot_enabled = cls.car_safety_mode_frame is not None
cls.can_msgs = sorted(can_msgs, key=lambda msg: msg.logMonoTime)
cls.CarInterface, cls.CarController, cls.CarState, cls.RadarInterface = interfaces[cls.platform]
cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, experimental_long, docs=False)
cls.CarInterface = interfaces[cls.platform]
cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, experimental_long, docs=False)
cls.CP_SP = cls.CarInterface.get_params_sp(cls.CP, cls.platform, cls.fingerprint, car_fw, experimental_long, docs=False)
assert cls.CP
assert cls.CP_SP
@@ -168,12 +166,15 @@ class TestCarModelBase(unittest.TestCase):
del cls.can_msgs
def setUp(self):
self.CI = self.CarInterface(self.CP.copy(), copy.deepcopy(self.CP_SP), self.CarController, self.CarState)
self.CI = self.CarInterface(self.CP.copy(), copy.deepcopy(self.CP_SP))
assert self.CI
# TODO: check safetyModel is in release panda build
self.safety = libsafety_py.libsafety
safety_param_sp = self.CP_SP.safetyParam
self.safety.set_current_safety_param_sp(safety_param_sp)
cfg = self.CP.safetyConfigs[-1]
set_status = self.safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam)
self.assertEqual(0, set_status, f"failed to set safetyModel {cfg}")
@@ -203,8 +204,8 @@ class TestCarModelBase(unittest.TestCase):
CC_SP = structs.CarControlSP()
for i, msg in enumerate(self.can_msgs):
CS = self.CI.update(can_capnp_to_list((msg.as_builder().to_bytes(),)))
self.CI.apply(CC, CC_SP, msg.logMonoTime)
CS = self.CI.update(msg)
self.CI.apply(CC, CC_SP, msg[0])
if CS.canValid:
can_valid = True
@@ -216,32 +217,32 @@ class TestCarModelBase(unittest.TestCase):
self.assertEqual(can_invalid_cnt, 0)
def test_radar_interface(self):
RI = self.RadarInterface(self.CP, self.CP_SP)
RI = self.CarInterface.RadarInterface(self.CP, self.CP_SP)
assert RI
# Since OBD port is multiplexed to bus 1 (commonly radar bus) while fingerprinting,
# start parsing CAN messages after we've left ELM mode and can expect CAN traffic
error_cnt = 0
for i, msg in enumerate(self.can_msgs[self.elm_frame:]):
rr: structs.RadarData | None = RI.update(can_capnp_to_list((msg.as_builder().to_bytes(),)))
rr: structs.RadarData | None = RI.update(msg)
if rr is not None and i > 50:
error_cnt += structs.RadarData.Error.canError in rr.errors
error_cnt += rr.errors.canError
self.assertEqual(error_cnt, 0)
def test_panda_safety_rx_checks(self):
if self.CP.dashcamOnly:
self.skipTest("no need to check panda safety for dashcamOnly")
start_ts = self.can_msgs[0].logMonoTime
start_ts = self.can_msgs[0][0]
failed_addrs = Counter()
for can in self.can_msgs:
# update panda timer
t = (can.logMonoTime - start_ts) / 1e3
t = (can[0] - start_ts) / 1e3
self.safety.set_timer(int(t))
# run all msgs through the safety RX hook
for msg in can.can:
for msg in can[1]:
if msg.src >= 64:
continue
@@ -279,7 +280,7 @@ class TestCarModelBase(unittest.TestCase):
def test_car_controller(car_control, car_control_sp):
now_nanos = 0
msgs_sent = 0
CI = self.CarInterface(self.CP, self.CP_SP, self.CarController, self.CarState)
CI = self.CarInterface(self.CP, self.CP_SP)
for _ in range(round(10.0 / DT_CTRL)): # make sure we hit the slowest messages
CI.update([])
_, sendcan = CI.apply(car_control, car_control_sp, now_nanos)
@@ -394,8 +395,8 @@ class TestCarModelBase(unittest.TestCase):
# warm up pass, as initial states may be different
for can in self.can_msgs[:300]:
self.CI.update(can_capnp_to_list((can.as_builder().to_bytes(), )))
for msg in filter(lambda m: m.src in range(64), can.can):
self.CI.update(can)
for msg in filter(lambda m: m.src < 64, can[1]):
to_send = libsafety_py.make_CANPacket(msg.address, msg.src % 4, msg.dat)
self.safety.safety_rx_hook(to_send)
@@ -404,8 +405,8 @@ class TestCarModelBase(unittest.TestCase):
checks = defaultdict(int)
vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar
for idx, can in enumerate(self.can_msgs):
CS = self.CI.update(can_capnp_to_list((can.as_builder().to_bytes(), ))).as_reader()
for msg in filter(lambda m: m.src in range(64), can.can):
CS = self.CI.update(can).as_reader()
for msg in filter(lambda m: m.src < 64, can[1]):
to_send = libsafety_py.make_CANPacket(msg.address, msg.src % 4, msg.dat)
ret = self.safety.safety_rx_hook(to_send)
self.assertEqual(1, ret, f"safety rx failed ({ret=}): {(msg.address, msg.src % 4)}")
+2 -2
View File
@@ -9,7 +9,7 @@ from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
from openpilot.common.swaglog import cloudlog
from opendbc.car.car_helpers import get_car_interface
from opendbc.car.car_helpers import interfaces
from opendbc.car.vehicle_model import VehicleModel
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
from openpilot.selfdrive.controls.lib.latcontrol import LatControl, MIN_LATERAL_CONTROL_SPEED
@@ -37,7 +37,7 @@ class Controls:
self.CP_SP = messaging.log_from_bytes(self.params.get("CarParamsSP", block=True), custom.CarParamsSP)
cloudlog.info("controlsd got CarParamsSP")
self.CI = get_car_interface(self.CP, self.CP_SP)
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
@@ -17,10 +17,10 @@ class TestLatControl:
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), (NISSAN.NISSAN_LEAF, LatControlAngle)])
def test_saturation(self, car_name, controller):
CarInterface, CarController, CarState, RadarInterface = interfaces[car_name]
CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(car_name)
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
CI = CarInterface(CP, CP_SP, CarController, CarState)
CI = CarInterface(CP, CP_SP)
VM = VehicleModel(CP)
controller = controller(CP.as_reader(), CI)
+1 -1
View File
@@ -20,7 +20,7 @@ def main():
longitudinal_planner = LongitudinalPlanner(CP)
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'],
poll='modelV2', ignore_avg_freq=['radarState'])
poll='modelV2')
while True:
sm.update()
+8 -14
View File
@@ -6,6 +6,7 @@ from typing import Any
import capnp
from cereal import messaging, log, car
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process
from openpilot.common.swaglog import cloudlog
@@ -51,7 +52,7 @@ class Track:
def __init__(self, identifier: int, v_lead: float, kalman_params: KalmanParams):
self.identifier = identifier
self.cnt = 0
self.aLeadTau = _LEAD_ACCEL_TAU
self.aLeadTau = FirstOrderFilter(_LEAD_ACCEL_TAU, 0.45, DT_MDL)
self.K_A = kalman_params.A
self.K_C = kalman_params.C
self.K_K = kalman_params.K
@@ -74,17 +75,12 @@ class Track:
# Learn if constant acceleration
if abs(self.aLeadK) < 0.5:
self.aLeadTau = _LEAD_ACCEL_TAU
self.aLeadTau.x = _LEAD_ACCEL_TAU
else:
self.aLeadTau *= 0.9
self.aLeadTau.update(0.0)
self.cnt += 1
def reset_a_lead(self, aLeadK: float, aLeadTau: float):
self.kf = KF1D([[self.vLead], [aLeadK]], self.K_A, self.K_C, self.K_K)
self.aLeadK = aLeadK
self.aLeadTau = aLeadTau
def get_RadarState(self, model_prob: float = 0.0):
return {
"dRel": float(self.dRel),
@@ -93,7 +89,7 @@ class Track:
"vLead": float(self.vLead),
"vLeadK": float(self.vLeadK),
"aLeadK": float(self.aLeadK),
"aLeadTau": float(self.aLeadTau),
"aLeadTau": float(self.aLeadTau.x),
"status": True,
"fcw": self.is_potential_fcw(model_prob),
"modelProb": model_prob,
@@ -211,9 +207,7 @@ class RadarD:
self.v_ego_hist.append(self.v_ego)
self.last_v_ego_frame = sm.recv_frame['carState']
ar_pts = {}
for pt in rr.points:
ar_pts[pt.trackId] = [pt.dRel, pt.yRel, pt.vRel, pt.measured]
ar_pts = {pt.trackId: [pt.dRel, pt.yRel, pt.vRel, pt.measured] for pt in rr.points}
# *** remove missing points from meta data ***
for ids in list(self.tracks.keys()):
@@ -233,10 +227,10 @@ class RadarD:
self.tracks[ids].update(rpt[0], rpt[1], rpt[2], v_lead, rpt[3])
# *** publish radarState ***
self.radar_state_valid = sm.all_checks() and len(rr.errors) == 0
self.radar_state_valid = sm.all_checks()
self.radar_state = log.RadarState.new_message()
self.radar_state.mdMonoTime = sm.logMonoTime['modelV2']
self.radar_state.radarErrors = list(rr.errors)
self.radar_state.radarErrors = rr.errors
self.radar_state.carStateMonoTime = sm.logMonoTime['carState']
if len(sm['modelV2'].velocity.x):
+1 -1
View File
@@ -26,6 +26,6 @@ class TestLeads:
msgs = [m for _ in range(3) for m in single_iter_pkg()]
out = replay_process_with_name("card", msgs, fingerprint=TOYOTA.TOYOTA_COROLLA_TSS2)
states = [m for m in out if m.which() == "liveTracks"]
failures = [not state.valid and len(state.liveTracks.errors) for state in states]
failures = [not state.valid for state in states]
assert len(states) == 0 or all(failures)
@@ -6,7 +6,6 @@ from tqdm import tqdm
from cereal import car
from opendbc.car.tests.routes import CarTestRoute
from openpilot.selfdrive.car.tests.test_models import TestCarModelBase
from openpilot.selfdrive.pandad import can_capnp_to_list
from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE
N_RUNS = 10
@@ -25,13 +24,11 @@ if __name__ == '__main__':
CC = car.CarControl.new_message()
ets = []
for _ in tqdm(range(N_RUNS)):
msgs = [m.as_builder().to_bytes() for m in tm.can_msgs]
start_t = time.process_time_ns()
for msg in msgs:
can_list = can_capnp_to_list([msg])
for msg in tm.can_msgs:
for cp in tm.CI.can_parsers.values():
if cp is not None:
cp.update_strings(can_list)
cp.update_strings(msg)
ets.append((time.process_time_ns() - start_t) * 1e-6)
print(f'{len(tm.can_msgs)} CAN packets, {N_RUNS} runs')
+4 -4
View File
@@ -10,21 +10,21 @@ if __name__ == "__main__":
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--fingerprint", help="The fingerprint to use")
parser.add_argument("route", help="The route name to use")
parser.add_argument("process", help="The process to run")
parser.add_argument("process", nargs='+', help="The process(s) to run")
args = parser.parse_args()
cfg = [c for c in CONFIGS if c.proc_name == args.process][0]
cfgs = [c for c in CONFIGS if c.proc_name in args.process]
lr = LogReader(args.route)
inputs = list(lr)
outputs = replay_process(cfg, inputs, fingerprint=args.fingerprint)
outputs = replay_process(cfgs, inputs, fingerprint=args.fingerprint)
# Remove message generated by the process under test and merge in the new messages
produces = {o.which() for o in outputs}
inputs = [i for i in inputs if i.which() not in produces]
outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime)
fn = f"{args.route.replace('/', '_')}_{args.process}.zst"
fn = f"{args.route.replace('/', '_')}_{'_'.join(args.process)}.zst"
print(f"Saving log to {fn}")
save_log(fn, outputs)
+5 -5
View File
@@ -52,14 +52,14 @@ class ParamsLearner:
device_pose = Pose.from_live_pose(msg)
calibrated_pose = self.calibrator.build_calibrated_pose(device_pose)
yaw_rate_valid = msg.angularVelocityDevice.valid and self.calibrator.calib_valid
yaw_rate_valid = msg.angularVelocityDevice.valid
yaw_rate_valid = yaw_rate_valid and 0 < self.yaw_rate_std < 10 # rad/s
yaw_rate_valid = yaw_rate_valid and abs(self.yaw_rate) < 1 # rad/s
if yaw_rate_valid:
self.yaw_rate, self.yaw_rate_std = calibrated_pose.angular_velocity.z, calibrated_pose.angular_velocity.z_std
else:
# This is done to bound the yaw rate estimate when localizer values are invalid or calibrating
self.yaw_rate, self.yaw_rate_std = 0.0, np.radians(1)
self.yaw_rate, self.yaw_rate_std = 0.0, np.radians(10.0)
localizer_roll, localizer_roll_std = device_pose.orientation.x, device_pose.orientation.x_std
localizer_roll_std = np.radians(1) if np.isnan(localizer_roll_std) else localizer_roll_std
@@ -225,13 +225,13 @@ def main():
liveParameters.posenetValid = True
liveParameters.sensorValid = sensors_valid
liveParameters.steerRatio = float(x[States.STEER_RATIO].item())
liveParameters.steerRatioValid = min_sr <= liveParameters.steerRatio <= max_sr
liveParameters.stiffnessFactor = float(x[States.STIFFNESS].item())
liveParameters.stiffnessFactorValid = 0.2 <= liveParameters.stiffnessFactor <= 5.0
liveParameters.roll = float(roll)
liveParameters.angleOffsetAverageDeg = float(angle_offset_average)
liveParameters.angleOffsetAverageValid = bool(avg_offset_valid)
liveParameters.angleOffsetDeg = float(angle_offset)
liveParameters.steerRatioValid = min_sr <= liveParameters.steerRatio <= max_sr
liveParameters.stiffnessFactorValid = 0.2 <= liveParameters.stiffnessFactor <= 5.0
liveParameters.angleOffsetAverageValid = bool(avg_offset_valid)
liveParameters.angleOffsetValid = bool(total_offset_valid)
liveParameters.valid = all((
liveParameters.angleOffsetAverageValid,
+2
View File
@@ -37,6 +37,7 @@ cl_mem* DrivingModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_hei
DrivingModelFrame::~DrivingModelFrame() {
deinit_transform();
loadyuv_destroy(&loadyuv);
CL_CHECK(clReleaseMemObject(input_frames_cl));
CL_CHECK(clReleaseMemObject(img_buffer_20hz_cl));
CL_CHECK(clReleaseMemObject(last_img_cl));
CL_CHECK(clReleaseCommandQueue(q));
@@ -58,5 +59,6 @@ cl_mem* MonitoringModelFrame::prepare(cl_mem yuv_cl, int frame_width, int frame_
MonitoringModelFrame::~MonitoringModelFrame() {
deinit_transform();
CL_CHECK(clReleaseMemObject(input_frame_cl));
CL_CHECK(clReleaseCommandQueue(q));
}
+21 -2
View File
@@ -62,8 +62,27 @@ void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16
handle->control_write(0xdc, (uint16_t)safety_model, safety_param);
}
void Panda::set_alternative_experience(uint16_t alternative_experience) {
handle->control_write(0xdf, alternative_experience, 0);
void Panda::set_alternative_experience(uint16_t alternative_experience, uint16_t safety_param_sp) {
handle->control_write(0xdf, alternative_experience, safety_param_sp);
}
std::string Panda::serial_read(int port_number) {
std::string ret;
char buffer[USBPACKET_MAX_SIZE] = {};
while (true) {
int bytes_read = handle->control_read(0xe0, port_number, 0, (unsigned char *)buffer, USBPACKET_MAX_SIZE);
if (bytes_read <= 0) {
break;
}
ret.append(buffer, bytes_read);
}
return ret;
}
void Panda::set_uart_baud(int uart, int rate) {
handle->control_write(0xe4, uart, int(rate / 300));
}
cereal::PandaState::PandaType Panda::get_hw_type() {
+3 -1
View File
@@ -63,7 +63,9 @@ public:
// Panda functionality
cereal::PandaState::PandaType get_hw_type();
void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U);
void set_alternative_experience(uint16_t alternative_experience);
void set_alternative_experience(uint16_t alternative_experience, uint16_t safety_param_sp=0U);
std::string serial_read(int port_number = 0);
void set_uart_baud(int uart, int rate);
void set_fan_speed(uint16_t fan_speed);
uint16_t get_fan_speed();
void set_ir_pwr(uint16_t ir_pwr);
+16 -7
View File
@@ -10,7 +10,8 @@ void PandaSafety::configureSafetyMode() {
auto car_params = fetchCarParams();
if (!car_params.empty()) {
LOGW("got %lu bytes CarParams", car_params.size());
LOGW("got %lu bytes CarParams", car_params[0].size());
LOGW("got %lu bytes CarParamsSP", car_params[1].size());
setSafetyMode(car_params);
safety_configured_ = true;
}
@@ -43,7 +44,8 @@ void PandaSafety::updateMultiplexingMode() {
}
}
std::string PandaSafety::fetchCarParams() {
// TODO-SP: Use structs instead of vector
std::vector<std::string> PandaSafety::fetchCarParams() {
if (!params_.getBool("FirmwareQueryDone")) {
return {};
}
@@ -56,16 +58,23 @@ std::string PandaSafety::fetchCarParams() {
if (!params_.getBool("ControlsReady")) {
return {};
}
return params_.get("CarParams");
return {params_.get("CarParams"), params_.get("CarParamsSP")};
}
void PandaSafety::setSafetyMode(const std::string &params_string) {
// TODO-SP: Use structs instead of vector
void PandaSafety::setSafetyMode(const std::vector<std::string> &params_string) {
AlignedBuffer aligned_buf;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(params_string.data(), params_string.size()));
AlignedBuffer aligned_buf_sp;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(params_string[0].data(), params_string[0].size()));
cereal::CarParams::Reader car_params = cmsg.getRoot<cereal::CarParams>();
capnp::FlatArrayMessageReader cmsg_sp(aligned_buf_sp.align(params_string[1].data(), params_string[1].size()));
cereal::CarParamsSP::Reader car_params_sp = cmsg_sp.getRoot<cereal::CarParamsSP>();
auto safety_configs = car_params.getSafetyConfigs();
uint16_t alternative_experience = car_params.getAlternativeExperience();
uint16_t safety_param_sp = car_params_sp.getSafetyParam();
for (int i = 0; i < pandas_.size(); ++i) {
// Default to SILENT safety model if not specified
@@ -76,8 +85,8 @@ void PandaSafety::setSafetyMode(const std::string &params_string) {
safety_param = safety_configs[i].getSafetyParam();
}
LOGW("Panda %d: setting safety model: %d, param: %d, alternative experience: %d", i, (int)safety_model, safety_param, alternative_experience);
pandas_[i]->set_alternative_experience(alternative_experience);
LOGW("Panda %d: setting safety model: %d, param: %d, alternative experience: %d, param_sp: %d", i, (int)safety_model, safety_param, alternative_experience, safety_param_sp);
pandas_[i]->set_alternative_experience(alternative_experience, safety_param_sp);
pandas_[i]->set_safety_model(safety_model, safety_param);
}
}
+10 -2
View File
@@ -41,7 +41,7 @@
#define CUTOFF_IL 400
#define SATURATE_IL 1000
#define ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE 2048
#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048
ExitHandler do_exit;
@@ -57,7 +57,7 @@ bool check_all_connected(const std::vector<Panda *> &pandas) {
bool process_mads_heartbeat(SubMaster *sm) {
const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience();
const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_DISENGAGE_LATERAL_ON_BRAKE) != 0;
const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0;
const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads();
const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled();
@@ -470,6 +470,14 @@ void pandad_run(std::vector<Panda *> &pandas) {
send_peripheral_state(peripheral_panda, &pm);
}
// Forward logs from pandas to cloudlog if available
for (auto *panda : pandas) {
std::string log = panda->serial_read();
if (!log.empty()) {
LOGD("%s", log.c_str());
}
}
rk.keepTime();
}
+2 -2
View File
@@ -16,8 +16,8 @@ public:
private:
void updateMultiplexingMode();
std::string fetchCarParams();
void setSafetyMode(const std::string &params_string);
std::vector<std::string> fetchCarParams();
void setSafetyMode(const std::vector<std::string> &params_string);
bool initialized_ = false;
bool log_once_ = false;
+5
View File
@@ -672,6 +672,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.NO_ENTRY: NoEntryAlert("Radar Error: Restart the Car"),
},
EventName.radarTempUnavailable: {
ET.SOFT_DISABLE: soft_disable_alert("Radar Temporarily Unavailable"),
ET.NO_ENTRY: NoEntryAlert("Radar Temporarily Unavailable"),
},
# Every frame from the camera should be processed by the model. If modeld
# is not processing frames fast enough they have to be dropped. This alert is
# thrown when over 20% of frames are dropped.
+9 -3
View File
@@ -92,7 +92,7 @@ class SelfdriveD(CruiseHelper):
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'controlsState', 'carControl', 'driverAssistance', 'alertDebug'] + \
self.camera_packets + self.sensor_packets + self.gps_packets,
ignore_alive=ignore, ignore_avg_freq=ignore+['radarState',],
ignore_alive=ignore, ignore_avg_freq=ignore,
ignore_valid=ignore, frequency=int(1/DT_CTRL))
# read params
@@ -294,7 +294,13 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.cameraFrameRate)
if not REPLAY and self.rk.lagging:
self.events.add(EventName.selfdrivedLagging)
if len(self.sm['radarState'].radarErrors) or ((not self.rk.lagging or REPLAY) and not self.sm.all_checks(['radarState'])):
if not self.sm.valid['radarState']:
if self.sm['radarState'].radarErrors.canError:
self.events.add(EventName.canError)
elif self.sm['radarState'].radarErrors.radarUnavailableTemporary:
self.events.add(EventName.radarTempUnavailable)
else:
self.events.add(EventName.radarFault)
self.events.add(EventName.radarFault)
if not self.sm.valid['pandaStates']:
self.events.add(EventName.usbError)
@@ -330,7 +336,7 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.posenetInvalid)
if not self.sm['livePose'].inputsOK:
self.events.add(EventName.locationdTemporaryError)
if not self.sm['liveParameters'].valid and not TESTING_CLOSET and (not SIMULATION or REPLAY):
if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
self.events.add(EventName.paramsdTemporaryError)
# conservative HW alert. if the data or frequency are off, locationd will throw an error
+6 -4
View File
@@ -241,14 +241,16 @@ def migrate_gpsLocation(msgs):
@migration(inputs=["deviceState", "initData"])
def migrate_deviceState(msgs):
init_data = next((m.initData for _, m in msgs if m.which() == 'initData'), None)
device_state = next((m.deviceState for _, m in msgs if m.which() == 'deviceState'), None)
if init_data is None or device_state is None:
return [], [], []
ops = []
dt = None
for i, msg in msgs:
if msg.which() == 'initData':
dt = msg.initData.deviceType
if msg.which() == 'deviceState':
n = msg.as_builder()
n.deviceState.deviceType = dt
n.deviceState.deviceType = init_data.deviceType
ops.append((i, n.as_reader()))
return ops, [], []
@@ -342,7 +342,7 @@ def card_fingerprint_callback(rc, pm, msgs, fingerprint):
def get_car_params_callback(rc, pm, msgs, fingerprint):
params = Params()
if fingerprint:
CarInterface, _, _, _ = interfaces[fingerprint]
CarInterface = interfaces[fingerprint]
CP = CarInterface.get_non_essential_params(fingerprint)
CP_SP = CarInterface.get_non_essential_params_sp(CP, fingerprint)
else:
+1 -1
View File
@@ -1 +1 @@
465979047295dad5da3a552db9227ed776a26a79
e9d57157494480637a8ffb52257d2b660a48be67
+1 -1
View File
@@ -391,7 +391,7 @@ class TestOnroad:
result += "----------------- Model Timing -----------------\n"
result += "------------------------------------------------\n"
cfgs = [
("modelV2", 0.045, 0.035),
("modelV2", 0.045, 0.040), # TODO: this should be stricter but it's hard to measure exactly
("driverStateV2", 0.045, 0.035),
]
for (s, instant_max, avg_max) in cfgs:
+15 -10
View File
@@ -79,31 +79,36 @@ bool HttpRequest::timeout() const {
return reply && reply->error() == QNetworkReply::OperationCanceledError;
}
void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) {
if (active()) {
qDebug() << "HttpRequest is active";
return;
}
QNetworkRequest HttpRequest::prepareRequest(const QString &requestURL) {
QNetworkRequest request;
QString token;
if (create_jwt) {
token = CommaApi::create_jwt();
token = GetJwtToken();
} else {
QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json"));
QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8());
token = json_d["access_token"].toString();
}
QNetworkRequest request;
request.setUrl(QUrl(requestURL));
request.setRawHeader("User-Agent", getUserAgent().toUtf8());
request.setRawHeader("User-Agent", GetUserAgent().toUtf8());
if (!token.isEmpty()) {
request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8());
}
return request;
}
if (method == HttpRequest::Method::GET) {
void HttpRequest::sendRequest(const QString &requestURL, const Method method) {
if (active()) {
qDebug() << "HttpRequest is active";
return;
}
QNetworkRequest request = prepareRequest(requestURL);
if (method == Method::GET) {
reply = nam()->get(request);
} else if (method == HttpRequest::Method::DELETE) {
} else if (method == Method::DELETE) {
reply = nam()->deleteResource(request);
}
+8 -5
View File
@@ -5,6 +5,7 @@
#include <QString>
#include <QTimer>
#include "util.h"
#include "common/util.h"
namespace CommaApi {
@@ -23,10 +24,11 @@ class HttpRequest : public QObject {
Q_OBJECT
public:
enum class Method {GET, DELETE};
enum class Method {GET, DELETE, POST, PUT};
explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000);
void sendRequest(const QString &requestURL, const Method method = Method::GET);
virtual void sendRequest(const QString &requestURL, Method method);
void sendRequest(const QString &requestURL) { sendRequest(requestURL, Method::GET);}
bool active() const;
bool timeout() const;
@@ -35,13 +37,14 @@ signals:
protected:
QNetworkReply *reply = nullptr;
private:
static QNetworkAccessManager *nam();
QTimer *networkTimer = nullptr;
bool create_jwt;
virtual QNetworkRequest prepareRequest(const QString& requestURL);
[[nodiscard]] virtual QString GetJwtToken() const { return CommaApi::create_jwt(); }
[[nodiscard]] virtual QString GetUserAgent() const { return getUserAgent(); }
private slots:
protected slots:
void requestTimeout();
void requestFinished();
};
+14 -1
View File
@@ -57,9 +57,17 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) {
});
addItem(hyundaiRadarTracksToggle);
auto enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), "");
enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), "");
addItem(enableGithubRunner);
// error log button
errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."));
connect(errorLogBtn, &ButtonControl::clicked, [=]() {
std::string txt = util::read_file("/data/community/crashes/error.log");
ConfirmationDialog::rich(QString::fromStdString(txt), this);
});
addItem(errorLogBtn);
// Joystick and longitudinal maneuvers should be hidden on release branches
is_release = params.getBool("IsReleaseBranch");
@@ -112,6 +120,11 @@ void DeveloperPanel::updateToggles(bool _offroad) {
}
experimentalLongitudinalToggle->refresh();
// Handle specific controls visibility for release branches
enableGithubRunner->setVisible(!is_release);
errorLogBtn->setVisible(!is_release);
joystickToggle->setVisible(!is_release);
offroad = _offroad;
}
@@ -16,9 +16,11 @@ private:
Params params;
ParamControl* adbToggle;
ParamControl* joystickToggle;
ButtonControl* errorLogBtn;
ParamControl* longManeuverToggle;
ParamControl* experimentalLongitudinalToggle;
ParamControl* hyundaiRadarTracksToggle;
ParamControl* enableGithubRunner;
bool is_release;
bool offroad = false;
+6 -1
View File
@@ -12,6 +12,10 @@
#include <QJsonObject>
#include <QTimer>
#ifdef SUNNYPILOT
#define UIState UIStateSP
#endif
FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent) {
layout = new QVBoxLayout(this);
layout->setContentsMargins(40, 40, 40, 40);
@@ -67,6 +71,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent)
"<br><br>"
"<b>Frequently Asked Questions</b><br><br>"
"<i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br>"
"<i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br>"
"<i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br>"
"<i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training."
));
@@ -85,7 +90,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent)
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
QJsonObject json = doc.object();
int count = json["firehose"].toInt();
contribution_label->setText(tr("<b>%1 %2</b> of your driving are in the training dataset so far.").arg(count).arg(count == 1 ? "segment" : "segments"));
contribution_label->setText(tr("<b>%n segment(s)</b> of your driving is in the training dataset so far.", "", count));
contribution_label->show();
}
});
+5 -1
View File
@@ -344,7 +344,11 @@ void SettingsWindow::setCurrentPanel(int index, const QString &param) {
// Find the panel by name
for (int i = 0; i < nav_btns->buttons().size(); i++) {
if (nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) {
bool panel_trimmed = false;
#ifdef SUNNYPILOT
panel_trimmed = nav_btns->buttons()[i]->text().trimmed() == tr(panelName.toStdString().c_str());
#endif
if ((nav_btns->buttons()[i]->text() == tr(panelName.toStdString().c_str())) || panel_trimmed) {
index = i;
break;
}
+2 -1
View File
@@ -48,7 +48,8 @@ void Setup::download(QString url) {
auto version = util::read_file("/VERSION");
struct curl_slist *list = NULL;
list = curl_slist_append(list, ("X-openpilot-serial: " + Hardware::get_serial()).c_str());
std::string header = "X-openpilot-serial: " + Hardware::get_serial();
list = curl_slist_append(list, header.c_str());
char tmpfile[] = "/tmp/installer_XXXXXX";
FILE *fp = fdopen(mkstemp(tmpfile), "wb");
+11 -1
View File
@@ -1,4 +1,5 @@
widgets_src = [
"sunnypilot/qt/request_repeater.cc",
"sunnypilot/qt/widgets/toggle.cc",
"sunnypilot/qt/widgets/controls.cc",
"sunnypilot/qt/widgets/drive_stats.cc",
@@ -13,6 +14,7 @@ qt_util = [
qt_src = [
"sunnypilot/ui.cc",
"sunnypilot/qt/api.cc",
"sunnypilot/qt/sidebar.cc",
"sunnypilot/qt/window.cc",
"sunnypilot/qt/home.cc",
@@ -21,6 +23,7 @@ qt_src = [
"sunnypilot/qt/offroad/settings/settings.cc",
"sunnypilot/qt/offroad/settings/software_panel.cc",
"sunnypilot/qt/offroad/settings/sunnylink_panel.cc",
"sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc",
"sunnypilot/qt/offroad/settings/sunnypilot_panel.cc",
"sunnypilot/qt/offroad/settings/trips_panel.cc",
"sunnypilot/qt/offroad/settings/vehicle_panel.cc",
@@ -35,11 +38,18 @@ sunnypilot_panel_qt_src = [
"sunnypilot/qt/offroad/settings/sunnypilot/mads_settings.cc",
]
network_src = [
"sunnypilot/qt/network/sunnylink/sunnylink_client.cc",
"sunnypilot/qt/network/sunnylink/services/base_device_service.cc",
"sunnypilot/qt/network/sunnylink/services/role_service.cc",
"sunnypilot/qt/network/sunnylink/services/user_service.cc",
]
vehicle_panel_qt_src = [
"sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc",
]
sp_widgets_src = widgets_src
sp_widgets_src = widgets_src + network_src
sp_qt_src = qt_src + sunnypilot_panel_qt_src + vehicle_panel_qt_src
sp_qt_util = qt_util
+63
View File
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/api.h"
#include <QApplication>
#include <QJsonDocument>
#include "util.h"
#include "selfdrive/ui/qt/util.h"
namespace SunnylinkApi {
QString create_jwt(const QJsonObject &payloads, int expiry, bool sunnylink) {
QJsonObject header = {{"alg", "RS256"}};
auto t = QDateTime::currentSecsSinceEpoch();
auto dongle_id = sunnylink ? getSunnylinkDongleId() : getDongleId();
QJsonObject payload = {{"identity", dongle_id.value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}};
for (auto it = payloads.begin(); it != payloads.end(); ++it) {
payload.insert(it.key(), it.value());
}
auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals;
QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' +
QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts);
auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256);
return jwt + "." + CommaApi::rsa_sign(hash).toBase64(b64_opts);
}
} // namespace SunnylinkApi
void HttpRequestSP::sendRequest(const QString& requestURL, Method method, const QByteArray& payload) {
if (active()) {
return;
}
QNetworkRequest request = prepareRequest(requestURL);
if(!payload.isEmpty() && (method == Method::POST || method == Method::PUT)) {
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
switch (method) {
case Method::GET:
reply = nam()->get(request);
break;
case Method::DELETE:
reply = nam()->deleteResource(request);
break;
case Method::POST:
reply = nam()->post(request, payload);
break;
case Method::PUT:
reply = nam()->put(request, payload);
break;
}
networkTimer->start();
connect(reply, &QNetworkReply::finished, this, &HttpRequestSP::requestFinished);
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include "selfdrive/ui/qt/api.h"
#include "selfdrive/ui/sunnypilot/qt/util.h"
#include "common/util.h"
namespace SunnylinkApi {
QByteArray rsa_encrypt(const QByteArray& data);
QByteArray rsa_decrypt(const QByteArray& data);
QString create_jwt(const QJsonObject& payloads = {}, int expiry = 3600, bool sunnylink = false);
}
class HttpRequestSP : public HttpRequest {
Q_OBJECT
public:
explicit HttpRequestSP(QObject* parent, bool create_jwt = true, int timeout = 20000, bool sunnylink = false) :
HttpRequest(parent, create_jwt, timeout), sunnylink(sunnylink) {}
using HttpRequest::sendRequest;
void sendRequest(const QString& requestURL, Method method, const QByteArray& payload);
private:
bool sunnylink;
protected:
[[nodiscard]] QString GetJwtToken() const override { return SunnylinkApi::create_jwt({}, 3600, sunnylink); }
[[nodiscard]] QString GetUserAgent() const override { return getUserAgent(sunnylink); }
};
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#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;
if (roleTypeString == "Sponsor") return RoleType::Sponsor;
return RoleType::Admin; // Default to 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);
}
};
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#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 Free
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); }
};
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#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;
}
};
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink//services/base_device_service.h"
#include "selfdrive/ui/sunnypilot/qt/request_repeater.h"
#include "common/swaglog.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h"
BaseDeviceService::BaseDeviceService(QObject* parent) : QObject(parent) {
param_watcher = new ParamWatcher(this);
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString &param_name, const QString &param_value) {
paramsRefresh();
});
param_watcher->addParam("SunnylinkEnabled");
}
void BaseDeviceService::paramsRefresh() {
}
void BaseDeviceService::loadDeviceData(const QString &url, bool poll) {
if (!is_sunnylink_enabled()) {
LOGW("Sunnylink is not enabled, refusing to load data.");
return;
}
auto sl_dongle_id = getSunnylinkDongleId();
if (!sl_dongle_id.has_value())
return;
QString fullUrl = SUNNYLINK_BASE_URL + "/device/" + *sl_dongle_id + url;
if (poll && !isCurrentyPolling()) {
LOGD("Polling %s", qPrintable(fullUrl));
LOGD("Cache key: SunnylinkCache_%s", qPrintable(QString(getCacheKey())));
repeater = new RequestRepeaterSP(this, fullUrl, "SunnylinkCache_" + getCacheKey(), 60, false, true);
connect(repeater, &RequestRepeaterSP::requestDone, this, &BaseDeviceService::handleResponse);
} else if (isCurrentyPolling()) {
repeater->ForceUpdate();
} else {
LOGD("Sending one-time %s", qPrintable(fullUrl));
initial_request = new HttpRequestSP(this, true, 10000, true);
connect(initial_request, &HttpRequestSP::requestDone, this, &BaseDeviceService::handleResponse);
}
}
void BaseDeviceService::stopPolling() {
if (repeater != nullptr) {
repeater->deleteLater();
repeater = nullptr;
}
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include "selfdrive/ui/sunnypilot/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;
HttpRequestSP* initial_request = nullptr;
RequestRepeaterSP* repeater = nullptr;
public:
explicit BaseDeviceService(QObject* parent = nullptr);
virtual QString getCacheKey() const = 0;
bool isCurrentyPolling() {return repeater != nullptr;}
void stopPolling();
};
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/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);
uiStateSP()->setSunnylinkRoles(roles);
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <vector>
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h"
#include "selfdrive/ui/sunnypilot/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";
};
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h"
#include <QJsonArray>
#include <QJsonDocument>
#include "selfdrive/ui/sunnypilot/ui.h"
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);
uiStateSP()->setSunnylinkDeviceUsers(users);
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <vector>
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h"
#include "selfdrive/ui/sunnypilot/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";
};
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h"
#include "selfdrive/ui/sunnypilot/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,22 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <QObject>
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h"
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h"
class SunnylinkClient : public QObject {
Q_OBJECT
public:
explicit SunnylinkClient(QObject* parent);
RoleService* role_service;
UserService* user_service;
};
@@ -0,0 +1,142 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h"
#include "selfdrive/ui/sunnypilot/ui.h"
#include "selfdrive/ui/sunnypilot/qt/api.h"
#include "selfdrive/ui/sunnypilot/qt/util.h"
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h"
// Sponsor Upsell
using qrcodegen::QrCode;
SunnylinkSponsorQRWidget::SunnylinkSponsorQRWidget(bool sponsor_pair, QWidget* parent) : QWidget(parent), sponsor_pair(sponsor_pair) {
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &SunnylinkSponsorQRWidget::refresh);
}
void SunnylinkSponsorQRWidget::showEvent(QShowEvent *event) {
refresh();
timer->start(5 * 60 * 1000);
device()->setOffroadBrightness(100);
}
void SunnylinkSponsorQRWidget::hideEvent(QHideEvent *event) {
timer->stop();
device()->setOffroadBrightness(BACKLIGHT_OFFROAD);
}
void SunnylinkSponsorQRWidget::refresh() {
QString qrString;
if (sponsor_pair) {
QString token = SunnylinkApi::create_jwt({}, 3600, true);
auto sl_dongle_id = getSunnylinkDongleId();
QByteArray payload = QString("1|" + sl_dongle_id.value_or("") + "|" + token).toUtf8().toBase64();
qrString = SUNNYLINK_BASE_URL + "/sso?state=" + payload;
} else {
qrString = "https://github.com/sponsors/sunnyhaibin";
}
this->updateQrCode(qrString);
update();
}
void SunnylinkSponsorQRWidget::updateQrCode(const QString &text) {
QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW);
qint32 sz = qr.getSize();
QImage im(sz, sz, QImage::Format_RGB32);
QRgb black = qRgb(0, 0, 0);
QRgb white = qRgb(255, 255, 255);
for (int y = 0; y < sz; y++) {
for (int x = 0; x < sz; x++) {
im.setPixel(x, y, qr.getModule(x, y) ? black : white);
}
}
// Integer division to prevent anti-aliasing
int final_sz = ((width() / sz) - 1) * sz;
img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly);
}
void SunnylinkSponsorQRWidget::paintEvent(QPaintEvent *e) {
QPainter p(this);
p.fillRect(rect(), Qt::white);
QSize s = (size() - img.size()) / 2;
p.drawPixmap(s.width(), s.height(), img);
}
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
auto vlayout = new QVBoxLayout();
vlayout->setContentsMargins(85, 70, 50, 70);
vlayout->setSpacing(50);
hlayout->addLayout(vlayout, 1);
{
auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this);
close->setIconSize(QSize(80, 80));
close->setStyleSheet("border: none;");
vlayout->addWidget(close, 0, Qt::AlignLeft);
connect(close, &QPushButton::clicked, this, [=] {
sunnylink_client->role_service->load();
sunnylink_client->user_service->load();
QDialog::reject();
});
//vlayout->addSpacing(30);
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);
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);
instructionsLabel->setStyleSheet("font-size: 47px; font-weight: bold; color: black;");
instructionsLabel->setWordWrap(true);
vlayout->addWidget(instructionsLabel);
vlayout->addStretch();
}
// QR code
auto *qr = new SunnylinkSponsorQRWidget(sponsor_pair, this);
hlayout->addWidget(qr, 1);
}
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <QrCode.hpp>
#include <QtCore/qjsonobject.h>
#include "common/util.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
const QString SUNNYLINK_BASE_URL = util::getenv("SUNNYLINK_API_HOST", "https://stg.api.sunnypilot.ai").c_str();
class SunnylinkSponsorQRWidget : public QWidget {
Q_OBJECT
public:
explicit SunnylinkSponsorQRWidget(bool sponsor_pair = false, QWidget* parent = 0);
void paintEvent(QPaintEvent*) override;
private:
QPixmap img;
QTimer *timer;
void updateQrCode(const QString &text);
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
bool sponsor_pair = false;
private slots:
void refresh();
};
// sponsor popup widget
class SunnylinkSponsorPopup : public DialogBase {
Q_OBJECT
public:
explicit SunnylinkSponsorPopup(bool sponsor_pair = false, QWidget* parent = 0);
private:
static QStringList getInstructions(bool sponsor_pair);
bool sponsor_pair = false;
};
@@ -12,12 +12,26 @@
SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
main_layout = new QStackedLayout(this);
ListWidget *list = new ListWidget(this, false);
sunnylink_client = new SunnylinkClient(this);
param_watcher = new ParamWatcher(this);
param_watcher->addParam("SunnylinkEnabled");
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString &param_name, const QString &param_value) {
paramsRefresh(param_name, param_value);
});
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
connect(uiStateSP(), &UIStateSP::sunnylinkRolesChanged, this, &SunnylinkPanel::updatePanel);
connect(uiStateSP(), &UIStateSP::sunnylinkDeviceUsersChanged, this, &SunnylinkPanel::updatePanel);
connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) {
is_onroad = !offroad;
updatePanel();
});
sunnylinkScreen = new QWidget(this);
QVBoxLayout *vlayout = new QVBoxLayout(sunnylinkScreen);
auto vlayout = new QVBoxLayout(sunnylinkScreen);
vlayout->setContentsMargins(50, 20, 50, 20);
auto *list = new ListWidget(this, false);
QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that.");
sunnylinkEnabledBtn = new ParamControl(
"SunnylinkEnabled",
@@ -26,23 +40,47 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
"");
list->addItem(sunnylinkEnabledBtn);
status_popup = new SunnylinkSponsorPopup(false, this);
sponsorBtn = new ButtonControlSP(
tr("Sponsor Status"), tr("SPONSOR"),
tr("Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."));
list->addItem(sponsorBtn);
connect(sponsorBtn, &ButtonControlSP::clicked, [=]() {
status_popup->exec();
});
list->addItem(horizontal_line());
pair_popup = new SunnylinkSponsorPopup(true, this);
pairSponsorBtn = new ButtonControlSP(
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, &ButtonControlSP::clicked, [=]() {
if (getSunnylinkDongleId().value_or(tr("N/A")) == "N/A") {
ConfirmationDialog::alert(tr("sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again."), this);
} else {
pair_popup->exec();
}
});
list->addItem(horizontal_line());
connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() {
// resets the description to the default one for the Easter egg
sunnylinkEnabledBtn->setDescription(sunnylinkEnabledBtnDesc);
});
connect(sunnylinkEnabledBtn, &ParamControl::toggleFlipped, [=](bool enabled) {
QString description;
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);
description = "<font color='SeaGreen'>"+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ "</font>";
} 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);
}
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>";
updatePanel(offroad);
}
sunnylinkEnabledBtn->showDescription();
sunnylinkEnabledBtn->setDescription(description);
updatePanel();
});
QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel);
@@ -51,22 +89,81 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
vlayout->addWidget(sunnylinkScroller);
main_layout->addWidget(sunnylinkScreen);
if (is_sunnylink_enabled) {
startSunnylink();
}
}
void SunnylinkPanel::paramsRefresh(const QString &param_name, const QString &param_value) {
// We do it on paramsRefresh because the toggleEvent happens before the value is updated
if (param_name == "SunnylinkEnabled" && param_value == "1") {
startSunnylink();
} else if (param_name == "SunnylinkEnabled" && param_value == "0") {
stopSunnylink();
}
updatePanel();
}
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::showEvent(QShowEvent *event) {
updatePanel(offroad);
updatePanel();
if (is_sunnylink_enabled) {
startSunnylink();
}
}
void SunnylinkPanel::updatePanel(bool _offroad) {
QString sunnylink_device_id = tr("Device ID") + " " + getSunnylinkDongleId().value_or(tr("N/A"));
sunnylinkEnabledBtn->setEnabled(_offroad);
if (sunnylinkEnabledBtn->isToggled()) {
sunnylinkEnabledBtn->setValue(sunnylink_device_id);
} else {
sunnylinkEnabledBtn->setValue("");
void SunnylinkPanel::updatePanel() {
if (!isVisible()) {
return;
}
offroad = _offroad;
const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A"));
sunnylinkEnabledBtn->setEnabled(!is_onroad);
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
bool is_sub = uiStateSP()->isSunnylinkSponsor() && is_sunnylink_enabled;
auto max_current_sponsor_rule = uiStateSP()->sunnylinkSponsorRole();
auto role_name = max_current_sponsor_rule.getSponsorTierString();
std::optional role_color = max_current_sponsor_rule.getSponsorTierColor();
bool is_paired = uiStateSP()->isSunnylinkPaired();
auto paired_users = uiStateSP()->sunnylinkDeviceUsers();
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(role_name.toStdString().c_str()) : tr("Not Sponsor"), role_color);
pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled);
pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired"));
if (!is_sunnylink_enabled) {
sunnylinkEnabledBtn->setValue("");
sponsorBtn->setValue("");
pairSponsorBtn->setValue("");
}
update();
}
@@ -7,7 +7,10 @@
#pragma once
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
class SunnylinkPanel : public QFrame {
@@ -16,16 +19,29 @@ class SunnylinkPanel : public QFrame {
public:
explicit SunnylinkPanel(QWidget *parent = nullptr);
void showEvent(QShowEvent *event) override;
void paramsRefresh(const QString&param_name, const QString&param_value);
public slots:
void updatePanel(bool _offroad);
void updatePanel();
private:
Params params;
QStackedLayout *main_layout = nullptr;
QWidget *sunnylinkScreen = nullptr;
ScrollViewSP *sunnylinkScroller = nullptr;
bool offroad;
SunnylinkSponsorPopup *status_popup;
SunnylinkSponsorPopup *pair_popup;
ButtonControlSP* sponsorBtn;
ButtonControlSP* pairSponsorBtn;
SunnylinkClient* sunnylink_client;
ParamControl *sunnylinkEnabledBtn;
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;
};
@@ -39,17 +39,19 @@ MadsSettings::MadsSettings(QWidget *parent) : QWidget(parent) {
"");
list->addItem(madsUnifiedEngagementModeToggle);
// Pause Lateral On Brake
std::vector<QString> lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering")};
madsPauseLateralOnBrake = new ButtonParamControl(
"MadsPauseLateralOnBrake",
tr("Steering Mode After Braking"),
// Steering Mode On Brake
std::vector<QString> lateral_on_brake_texts{tr("Remain Active"), tr("Pause Steering"), tr("Disengage")};
madsSteeringMode = new ButtonParamControl(
"MadsSteeringMode",
tr("Steering Mode on Brake Pedal"),
tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.\n\n"
"Remain Active: ALC will remain active even after the brake pedal is pressed.\nPause Steering: ALC will be paused after the brake pedal is manually pressed."),
"Remain Active: ALC will remain active even after the brake pedal is pressed.\n"
"Pause Steering: ALC will be paused when the brake pedal is manually pressed."),
"Disengage: ALC will be disengaged after the brake pedal is pressed.\n"
"",
lateral_on_brake_texts,
500);
list->addItem(madsPauseLateralOnBrake);
list->addItem(madsSteeringMode);
QObject::connect(uiState(), &UIState::offroadTransition, this, &MadsSettings::updateToggles);
@@ -61,7 +63,7 @@ void MadsSettings::showEvent(QShowEvent *event) {
}
void MadsSettings::updateToggles(bool _offroad) {
madsPauseLateralOnBrake->setEnabled(_offroad);
madsSteeringMode->setEnabled(_offroad);
offroad = _offroad;
}
@@ -32,5 +32,5 @@ private:
ParamControl *madsMainCruiseToggle;
ParamControl *madsUnifiedEngagementModeToggle;
ButtonParamControl *madsPauseLateralOnBrake;
ButtonParamControl *madsSteeringMode;
};
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/request_repeater.h"
RequestRepeaterSP::RequestRepeaterSP(QObject *parent, const QString &requestURL, const QString &cacheKey,
int period, bool whileOnroad, bool sunnylink) : HttpRequestSP(parent, true, 20000, sunnylink) {
request_url = requestURL;
while_onroad = whileOnroad;
timer = new QTimer(this);
timer->setTimerType(Qt::VeryCoarseTimer);
connect(timer, &QTimer::timeout, [=]() { this->timerTick(); });
timer->start(period * 1000);
if (!cacheKey.isEmpty()) {
prevResp = QString::fromStdString(params.get(cacheKey.toStdString()));
if (!prevResp.isEmpty()) {
QTimer::singleShot(500, [=]() { emit requestDone(prevResp, true, QNetworkReply::NoError); });
}
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 RequestRepeaterSP::timerTick() {
if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) {
LOGD("Sending request for %s", qPrintable(request_url));
sendRequest(request_url);
}
}
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include "selfdrive/ui/qt/request_repeater.h"
#include "common/swaglog.h"
#include "common/util.h"
#include "selfdrive/ui/sunnypilot/ui.h"
#include "selfdrive/ui/sunnypilot/qt/api.h"
class RequestRepeaterSP : public HttpRequestSP {
private:
Params params;
QTimer *timer;
QString prevResp;
QString request_url;
bool while_onroad;
void timerTick();
public:
RequestRepeaterSP(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();
}
};
+10
View File
@@ -48,6 +48,16 @@ UIStateSP *uiStateSP() {
return &ui_state;
}
void UIStateSP::setSunnylinkRoles(const std::vector<RoleModel>& roles) {
sunnylinkRoles = roles;
emit sunnylinkRolesChanged(roles);
}
void UIStateSP::setSunnylinkDeviceUsers(const std::vector<UserModel>& users) {
sunnylinkUsers = users;
emit sunnylinkDeviceUsersChanged(users);
}
DeviceSP *deviceSP() {
static DeviceSP _device;
return &_device;
+47
View File
@@ -7,6 +7,11 @@
#pragma once
#include <optional>
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h"
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h"
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h"
#include "selfdrive/ui/ui.h"
class UIStateSP : public UIState {
@@ -15,12 +20,54 @@ class UIStateSP : public UIState {
public:
UIStateSP(QObject *parent = 0);
void updateStatus() override;
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";
});
}
signals:
void sunnylinkRoleChanged(bool subscriber);
void sunnylinkRolesChanged(std::vector<RoleModel> roles);
void sunnylinkDeviceUsersChanged(std::vector<UserModel> users);
void uiUpdate(const UIStateSP &s);
private slots:
void update() override;
private:
std::vector<RoleModel> sunnylinkRoles = {};
std::vector<UserModel> sunnylinkUsers = {};
};
UIStateSP *uiStateSP();
+6
View File
@@ -207,6 +207,11 @@ def setup_settings_sunnylink(click, pm: PubMaster, scroll=None):
click(278, 522)
time.sleep(UI_DELAY)
def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None):
setup_settings_sunnylink(click, pm)
click(1967, 225)
time.sleep(UI_DELAY)
def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None):
setup_settings_device(click, pm)
click(278, 852)
@@ -268,6 +273,7 @@ CASES = {
CASES.update({
"settings_sunnylink": setup_settings_sunnylink,
"settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button,
"settings_sunnypilot": setup_settings_sunnypilot,
"settings_sunnypilot_mads": setup_settings_sunnypilot_mads,
"settings_trips": setup_settings_trips,
+124 -20
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished">عرض</translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -419,33 +431,42 @@
<name>FirehosePanel</name>
<message>
<source>🔥 Firehose Mode 🔥</source>
<translation type="unfinished"></translation>
<translation>🔥 وضع خرطوم الحريق 🔥</translation>
</message>
<message>
<source>openpilot learns to drive by watching humans, like you, drive.
Firehose Mode allows you to maximize your training data uploads to improve openpilot&apos;s driving models. More data means bigger models, which means better Experimental Mode.</source>
<translation type="unfinished"></translation>
<translation>يتعلم تطبيق openpilot كيفية القيادة من خلال مشاهدة البشر، مثلك، أثناء القيادة.
يتيح لك وضع خرطوم الحريق زيادة تحميلات بيانات التدريب لتحسين نماذج القيادة في OpenPilot. كلما زادت البيانات، زادت النماذج، مما يعني وضعًا تجريبيًا أفضل.</translation>
</message>
<message>
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
<translation>وضع خرطوم الحريق: نشط</translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
<translation>نشط</translation>
</message>
<message>
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
<translation>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;غير نشط&lt;/span&gt;: اتصل بشبكة غير محسوبة</translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation>للحصول على أقصى فعالية، أحضر جهازك إلى الداخل واتصل بمحول USB-C جيد وشبكة Wi-Fi أسبوعياً.&lt;br&gt;&lt;br&gt;يمكن أن يعمل وضع خرطوم الحريق أيضاً أثناء القيادة إذا كنت متصلاً بنقطة اتصال أو ببطاقة SIM غير محدودة.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;الأسئلة المتكررة&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;هل يهم كيف أو أين أقود؟&lt;/i&gt; لا، فقط قد كما تفعل عادة.&lt;br&gt;&lt;br&gt;&lt;i&gt;هل يتم سحب كل مقاطع رحلاتي في وضع خرطوم الحريق؟&lt;/i&gt; لا، نقوم بسحب مجموعة مختارة من مقاطع رحلاتك.&lt;br&gt;&lt;br&gt;&lt;i&gt;ما هو محول USB-C الجيد؟&lt;/i&gt; أي شاحن سريع للهاتف أو اللابتوب يجب أن يكون مناسباً.&lt;br&gt;&lt;br&gt;&lt;i&gt;هل يهم أي برنامج أستخدم؟&lt;/i&gt; نعم، فقط النسخة الأصلية من openpilot (وأفرع معينة) يمكن استخدامها للتدريب.</translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation>
<numerusform>حتى الآن، يوجد &lt;/b&gt;%n مقطع&lt;b&gt;%n من قيادتك في مجموعة بيانات التدريب.</numerusform>
<numerusform>حتى الآن، يوجد &lt;/b&gt;%n مقطع&lt;b&gt;%n من قيادتك في مجموعة بيانات التدريب.</numerusform>
<numerusform>حتى الآن، يوجد &lt;/b&gt;%n مقطع&lt;b&gt;%n من قيادتك في مجموعة بيانات التدريب.</numerusform>
<numerusform>حتى الآن، يوجد &lt;/b&gt;%n مقطع&lt;b&gt;%n من قيادتك في مجموعة بيانات التدريب.</numerusform>
<numerusform>حتى الآن، يوجد &lt;/b&gt;%n مقطع&lt;b&gt;%n من قيادتك في مجموعة بيانات التدريب.</numerusform>
<numerusform>حتى الآن، يوجد &lt;/b&gt;%n مقطع&lt;b&gt;%n من قيادتك في مجموعة بيانات التدريب.</numerusform>
</translation>
</message>
</context>
<context>
@@ -925,7 +946,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>Firehose</source>
<translation type="unfinished"></translation>
<translation>خرطوم الحريق</translation>
</message>
</context>
<context>
@@ -972,7 +993,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>Firehose</source>
<translation type="unfinished"></translation>
<translation type="unfinished">خرطوم الحريق</translation>
</message>
</context>
<context>
@@ -1441,6 +1462,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished">غير متاح</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished">إقران</translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
@@ -1469,11 +1573,11 @@ This may take up to a minute.</source>
</message>
<message>
<source>Welcome to openpilot</source>
<translation type="unfinished"></translation>
<translation>مرحباً بكم في openpilot</translation>
</message>
<message>
<source>You must accept the Terms and Conditions to use openpilot. Read the latest terms at &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; before continuing.</source>
<translation type="unfinished"></translation>
<translation>يجب عليك قبول الشروط والأحكام لاستخدام openpilot. اقرأ أحدث الشروط على &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; قبل الاستمرار.</translation>
</message>
</context>
<context>
@@ -1634,15 +1738,15 @@ This may take up to a minute.</source>
<name>WiFiPromptWidget</name>
<message>
<source>Open</source>
<translation type="unfinished"></translation>
<translation>انفتح</translation>
</message>
<message>
<source>Maximize your training data uploads to improve openpilot&apos;s driving models.</source>
<translation type="unfinished"></translation>
<translation>قم بزيادة تحميلات بيانات التدريب الخاصة بك لتحسين نماذج القيادة الخاصة بـ openpilot.</translation>
</message>
<message>
<source>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; Firehose Mode &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</source>
<translation type="unfinished"></translation>
<translation>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; وضع خرطوم الحريق &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</translation>
</message>
</context>
<context>
+106 -8
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished">ANSEHEN</translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -431,14 +443,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
@@ -447,6 +451,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>HudRenderer</name>
@@ -1425,6 +1440,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished">Nicht verfügbar</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
+10
View File
@@ -1,6 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US">
<context>
<name>FirehosePanel</name>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation>
<numerusform>&lt;b&gt;%n segment&lt;/b&gt; of your driving is in the training dataset so far.</numerusform>
<numerusform>&lt;b&gt;%n segments&lt;/b&gt; of your driving are in the training dataset so far.</numerusform>
</translation>
</message>
</context>
<context>
<name>InputDialog</name>
<message numerus="yes">
+110 -8
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished">VER</translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -431,14 +443,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
@@ -447,6 +451,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>HudRenderer</name>
@@ -1425,6 +1440,93 @@ Esto puede tardar un minuto.</translation>
<source>N/A</source>
<translation type="unfinished">N/A</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished">Estado de patrocinio</translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished">PATROCINADOR</translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished">Conviértete en patrocinador de sunnypilot para obtener acceso anticipado a las funciones de sunnylink cuando estén disponibles.</translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished">Emparejar cuenta de GitHub</translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished">EMPAREJAR</translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished">Empareja tu cuenta de GitHub para otorgar a tu dispositivo beneficios de patrocinador, incluido acceso a la API en sunnylink.</translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished">ID del dongle de sunnylink no encontrado. Esto puede deberse a una conexión débil o a un problema de registro en sunnylink. Por favor, reinicia y vuelve a intentarlo.</translation>
</message>
<message>
<source>THANKS</source>
<translation type="obsolete">GRACIAS</translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished">No patrocinador</translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished">Emparejado</translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished">No emparejado</translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished">GRACIAS </translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished">Escanea el código QR para iniciar sesión en tu cuenta de GitHub</translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished">Sigue las indicaciones para completar el proceso de emparejamiento</translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished">Vuelve a ingresar al panel de &quot;sunnylink&quot; para verificar el estado de patrocinio</translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished">Si el estado de patrocinio no se actualizó, por favor contacta a un moderador en Discord en https://discord.gg/sunnypilot</translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished">Escanea el código QR para visitar la página de GitHub Sponsors de sunnyhaibin</translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished">Elige tu nivel de patrocinio y confirma tu apoyo</translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished">Únete a nuestra comunidad en Discord en https://discord.gg/sunnypilot y contacta a un moderador para confirmar tu estado de patrocinio</translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished">Empareja tu cuenta de GitHub</translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished">Acceso temprano: conviértete en patrocinador de sunnypilot</translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
+106 -8
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished">VOIR</translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -431,14 +443,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
@@ -447,6 +451,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>HudRenderer</name>
@@ -1425,6 +1440,89 @@ Cela peut prendre jusqu&apos;à une minute.</translation>
<source>N/A</source>
<translation type="unfinished">N/A</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished">ASSOCIER</translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
+106 -9
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -431,14 +443,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
@@ -447,6 +451,16 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>HudRenderer</name>
@@ -952,7 +966,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>Firehose</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -1421,6 +1435,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished">OK</translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
+120 -21
View File
@@ -129,11 +129,11 @@
</message>
<message>
<source>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</source>
<translation>경고: openpilot (AEB) .</translation>
<translation>경고: openpilot (AEB) .</translation>
</message>
<message>
<source>On this car, openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.</source>
<translation> openpilot ACC로 . openpilot의 . openpilot .</translation>
<translation> openpilot은 openpilot ACC로 . openpilot . openpilot .</translation>
</message>
<message>
<source>Enable ADB</source>
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -425,27 +437,31 @@
<source>openpilot learns to drive by watching humans, like you, drive.
Firehose Mode allows you to maximize your training data uploads to improve openpilot&apos;s driving models. More data means bigger models, which means better Experimental Mode.</source>
<translation type="unfinished"></translation>
<translation> .
. , .</translation>
</message>
<message>
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
<translation> 모드: 활성화</translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
<translation>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;&lt;/span&gt;: </translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation> USB-C Wi-Fi에 .&lt;br&gt;&lt;br&gt; SIM .&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt; &lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;운전 방법이나 장소가 중요한가요?&lt;/i&gt; , .&lt;br&gt;&lt;br&gt;&lt;i&gt; ?.&lt;br&gt;&lt;br&gt;&lt;i&gt; , .&lt;br&gt;&lt;br&gt;&lt;i&gt; USB-C ?&lt;/i&gt; 휴대폰이나 노트북 고속 충전기가 있으면 됩니다.&lt;br&gt;&lt;br&gt;&lt;i&gt;어떤 소프트웨어를 실행하는지가 중요한가요?&lt;/i&gt; , openpilot의 .</translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
@@ -604,7 +620,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.</s
</message>
<message>
<source>openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.</source>
<translation>openpilot이 . ECU가 . PR을 . discord.comma.ai에 .</translation>
<translation>openpilot이 . ECU가 . PR을 . discord.comma.ai에 .</translation>
</message>
<message>
<source>openpilot detected a change in the device&apos;s mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.</source>
@@ -612,7 +628,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.</s
</message>
<message>
<source>Device temperature too high. System cooling down before starting. Current internal component temperature: %1</source>
<translation> . . : %1</translation>
<translation> . . : %1</translation>
</message>
<message>
<source>sunnypilot is now in Always Offroad mode. sunnypilot won&apos;t start until Always Offroad mode is disabled. Go to &quot;Settings&quot; -&gt; &quot;Device&quot; to exit Always Offroad mode.</source>
@@ -952,7 +968,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>Firehose</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -1421,6 +1437,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished">N/A</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
@@ -1449,11 +1548,11 @@ This may take up to a minute.</source>
</message>
<message>
<source>Welcome to openpilot</source>
<translation type="unfinished"></translation>
<translation> .</translation>
</message>
<message>
<source>You must accept the Terms and Conditions to use openpilot. Read the latest terms at &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; before continuing.</source>
<translation type="unfinished"></translation>
<translation> . &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; 에서 최신 약관을 읽은 후 계속하세요.</translation>
</message>
</context>
<context>
@@ -1556,7 +1655,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.</source>
<translation> . .</translation>
<translation> . .</translation>
</message>
<message>
<source>Always-On Driver Monitoring</source>
@@ -1564,7 +1663,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>Enable driver monitoring even when openpilot is not engaged.</source>
<translation>Openpilot이 .</translation>
<translation>openpilot이 .</translation>
</message>
<message>
<source>Enable Dynamic Experimental Control</source>
+117 -17
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished">VER</translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -419,33 +431,38 @@
<name>FirehosePanel</name>
<message>
<source>🔥 Firehose Mode 🔥</source>
<translation>🔥 Mode Firehose 🔥</translation>
<translation>🔥 Modo Firehose 🔥</translation>
</message>
<message>
<source>openpilot learns to drive by watching humans, like you, drive.
Firehose Mode allows you to maximize your training data uploads to improve openpilot&apos;s driving models. More data means bigger models, which means better Experimental Mode.</source>
<translation type="unfinished"></translation>
<translation>O openpilot aprende a dirigir observando humanos, como você, dirigirem.
O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar os modelos de direção do openpilot. Mais dados significam modelos maiores, o que resulta em um Modo Experimental melhor.</translation>
</message>
<message>
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
<translation>Modo Firehose: ATIVO</translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
<translation>ATIVO</translation>
</message>
<message>
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
<translation>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INATIVO&lt;/span&gt;: conecte-se a uma rede sem restrição &lt;br&gt; de dados</translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation>Para maior eficácia, leve seu dispositivo para dentro de casa e conecte-o a um bom adaptador USB-C e Wi-Fi semanalmente.&lt;br&gt;&lt;br&gt;O Modo Firehose também pode funcionar enquanto você dirige, se estiver conectado a um hotspot ou a um chip SIM com dados ilimitados.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Perguntas Frequentes&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Importa como ou onde eu dirijo?&lt;/i&gt; Não, basta dirigir normalmente.&lt;br&gt;&lt;br&gt;&lt;i&gt;Todos os meus segmentos são enviados no Modo Firehose?&lt;/i&gt; Não, selecionamos apenas um subconjunto dos seus segmentos.&lt;br&gt;&lt;br&gt;&lt;i&gt;Qual é um bom adaptador USB-C?&lt;/i&gt; Qualquer carregador rápido de telefone ou laptop deve ser suficiente.&lt;br&gt;&lt;br&gt;&lt;i&gt;Importa qual software eu uso?&lt;/i&gt; Sim, apenas o openpilot oficial (e alguns forks específicos) podem ser usados para treinamento.</translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation>
<numerusform>&lt;b&gt;%n segmento&lt;/b&gt; da sua direção está no conjunto de dados de treinamento até agora.</numerusform>
<numerusform>&lt;b&gt;%n segmentos&lt;/b&gt; da sua direção estão no conjunto de dados de treinamento até agora.</numerusform>
</translation>
</message>
</context>
<context>
@@ -956,7 +973,7 @@ Isso pode levar até um minuto.</translation>
</message>
<message>
<source>Firehose</source>
<translation type="unfinished"></translation>
<translation type="unfinished">Firehose</translation>
</message>
</context>
<context>
@@ -1425,6 +1442,89 @@ Isso pode levar até um minuto.</translation>
<source>N/A</source>
<translation type="unfinished">N/A</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished">PAREAR</translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
@@ -1453,11 +1553,11 @@ Isso pode levar até um minuto.</translation>
</message>
<message>
<source>Welcome to openpilot</source>
<translation type="unfinished"></translation>
<translation>Bem vindo ao openpilot</translation>
</message>
<message>
<source>You must accept the Terms and Conditions to use openpilot. Read the latest terms at &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; before continuing.</source>
<translation type="unfinished"></translation>
<translation>Você deve aceitar os Termos e Condições para usar o openpilot. Leia os termos mais recentes em &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; antes de continuar.</translation>
</message>
</context>
<context>
@@ -1626,7 +1726,7 @@ Isso pode levar até um minuto.</translation>
</message>
<message>
<source>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; Firehose Mode &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</source>
<translation>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; Mode Firehose &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</translation>
<translation>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; Modo Firehose &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</translation>
</message>
</context>
<context>
+105 -8
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -431,14 +443,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
@@ -447,6 +451,16 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>HudRenderer</name>
@@ -1421,6 +1435,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
+105 -8
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished">BAK</translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -431,14 +443,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
@@ -447,6 +451,16 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>HudRenderer</name>
@@ -1419,6 +1433,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished">N/A</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
+114 -15
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -425,27 +437,31 @@
<source>openpilot learns to drive by watching humans, like you, drive.
Firehose Mode allows you to maximize your training data uploads to improve openpilot&apos;s driving models. More data means bigger models, which means better Experimental Mode.</source>
<translation type="unfinished"></translation>
<translation>openpilot
openpilot </translation>
</message>
<message>
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
<translation>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;&lt;/span&gt;</translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation> USB-C Wi-Fi&lt;br&gt;&lt;br&gt;Firehose 使 SIM &lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;我开车的方式或地点有影响吗?&lt;/i&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Firehose &lt;/i&gt;不会,我们会选择性地上传部分片段。&lt;br&gt;&lt;br&gt;&lt;i&gt;什么是好的 USB-C 充电器?&lt;/i&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;使&lt;/i&gt; openpilot</translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation>
<numerusform>&lt;b&gt; %n &lt;/b&gt; </numerusform>
</translation>
</message>
</context>
<context>
@@ -952,7 +968,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>Firehose</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -1421,6 +1437,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished">N/A</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
@@ -1449,11 +1548,11 @@ This may take up to a minute.</source>
</message>
<message>
<source>Welcome to openpilot</source>
<translation type="unfinished"></translation>
<translation>使 openpilot</translation>
</message>
<message>
<source>You must accept the Terms and Conditions to use openpilot. Read the latest terms at &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; before continuing.</source>
<translation type="unfinished"></translation>
<translation>使 openpilot&lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt;。</translation>
</message>
</context>
<context>
+114 -15
View File
@@ -159,6 +159,18 @@
<source>Enables or disables the github runner service.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error Log</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>VIEW</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>View the error log for sunnypilot crashes.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
@@ -425,27 +437,31 @@
<source>openpilot learns to drive by watching humans, like you, drive.
Firehose Mode allows you to maximize your training data uploads to improve openpilot&apos;s driving models. More data means bigger models, which means better Experimental Mode.</source>
<translation type="unfinished"></translation>
<translation>openpilot
openpilot </translation>
</message>
<message>
<source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;b&gt;%1 %2&lt;/b&gt; of your driving are in the training dataset so far.</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>ACTIVE</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to unmetered network</source>
<translation type="unfinished"></translation>
<translation>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;&lt;/span&gt;</translation>
</message>
<message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation> USB-C Wi-Fi&lt;br&gt;&lt;br&gt;使 SIM &lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;我開車的方式或地點有影響嗎?&lt;/i&gt; &lt;br&gt;&lt;br&gt;&lt;i&gt;&lt;/i&gt;不會,我們會選擇性地上傳部分片段。&lt;br&gt;&lt;br&gt;&lt;i&gt;什麼是好的 USB-C 充電器?&lt;/i&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;使&lt;/i&gt; openpilot</translation>
</message>
<message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation>
<numerusform>&lt;b&gt; %n &lt;/b&gt; </numerusform>
</translation>
</message>
</context>
<context>
@@ -952,7 +968,7 @@ This may take up to a minute.</source>
</message>
<message>
<source>Firehose</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@@ -1421,6 +1437,89 @@ This may take up to a minute.</source>
<source>N/A</source>
<translation type="unfinished">使</translation>
</message>
<message>
<source>Sponsor Status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SPONSOR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair GitHub Account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Sponsor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Not Paired</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>THANKS </source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnylinkSponsorPopup</name>
<message>
<source>Scan the QR code to login to your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Follow the prompts to complete the pairing process</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Re-enter the &quot;sunnylink&quot; panel to verify sponsorship status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Scan the QR code to visit sunnyhaibin&apos;s GitHub Sponsors page</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Choose your sponsorship tier and confirm your support</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pair your GitHub account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Early Access: Become a sunnypilot Sponsor</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SunnypilotPanel</name>
@@ -1449,11 +1548,11 @@ This may take up to a minute.</source>
</message>
<message>
<source>Welcome to openpilot</source>
<translation type="unfinished"></translation>
<translation>使 openpilot</translation>
</message>
<message>
<source>You must accept the Terms and Conditions to use openpilot. Read the latest terms at &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; before continuing.</source>
<translation type="unfinished"></translation>
<translation>使 openpilot&lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt;。</translation>
</message>
</context>
<context>
+42 -48
View File
@@ -1,63 +1,57 @@
"""
The MIT License
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Last updated: July 29, 2024
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.common.params import Params
from opendbc.car import structs
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from opendbc.car.hyundai.values import HyundaiSafetyFlags
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP
class MadsParams:
def __init__(self):
self.params = Params()
class MadsSteeringModeOnBrake:
REMAIN_ACTIVE = 0
PAUSE = 1
DISENGAGE = 2
def read_param(self, key: str):
return self.params.get_bool(key)
def set_alternative_experience(self, CP):
enabled = self.read_param("Mads")
pause_lateral_on_brake = self.read_param("MadsPauseLateralOnBrake")
def read_steering_mode_param(params: Params):
try:
return int(params.get("MadsSteeringMode"))
except (ValueError, TypeError):
return f"{MadsSteeringModeOnBrake.REMAIN_ACTIVE}"
if enabled:
CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS
if pause_lateral_on_brake:
CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISENGAGE_LATERAL_ON_BRAKE
def set_alternative_experience(CP: structs.CarParams, params: Params):
enabled = params.get_bool("Mads")
steering_mode = read_steering_mode_param(params)
def set_car_specific_params(self, CP, CP_SP):
if CP.brand == "hyundai":
# TODO-SP: This should be separated from MADS module for future implementations
# Use "HyundaiLongitudinalMainCruiseToggleable" param
hyundai_cruise_main_toggleable = True
if hyundai_cruise_main_toggleable:
CP_SP.flags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value
CP.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.FLAG_HYUNDAI_LONG_MAIN_CRUISE_TOGGLEABLE.value
if enabled:
CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ENABLE_MADS
# MADS is currently not supported in Tesla due to lack of consistent states to engage controls
# TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling
if CP.brand == "tesla":
self.params.remove("Mads")
if steering_mode == MadsSteeringModeOnBrake.DISENGAGE:
CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_DISENGAGE_LATERAL_ON_BRAKE
elif steering_mode == MadsSteeringModeOnBrake.PAUSE:
CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.MADS_PAUSE_LATERAL_ON_BRAKE
def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params):
if CP.brand == "hyundai":
# TODO-SP: This should be separated from MADS module for future implementations
# Use "HyundaiLongitudinalMainCruiseToggleable" param
hyundai_cruise_main_toggleable = True
if hyundai_cruise_main_toggleable:
CP_SP.flags |= HyundaiFlagsSP.LONGITUDINAL_MAIN_CRUISE_TOGGLEABLE.value
CP_SP.safetyParam |= HyundaiSafetyFlagsSP.LONG_MAIN_CRUISE_TOGGLEABLE
# MADS is currently not supported in Tesla due to lack of consistent states to engage controls
# TODO-SP: To enable MADS for Tesla, identify consistent signals for MADS toggling
if CP.brand == "tesla":
params.remove("Mads")
# MADS is currently not supported in Rivian due to lack of consistent states to engage controls
# TODO-SP: To enable MADS for Rivian, identify consistent signals for MADS toggling
if CP.brand == "rivian":
params.remove("Mads")
+18 -32
View File
@@ -1,34 +1,14 @@
"""
The MIT License
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Last updated: July 29, 2024
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import car, log, custom
from opendbc.car.hyundai.values import HyundaiFlags
from openpilot.sunnypilot.mads.helpers import MadsParams
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param
from openpilot.sunnypilot.mads.state import StateMachine, GEARS_ALLOW_PAUSED_SILENT
State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
@@ -43,7 +23,7 @@ IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
class ModularAssistiveDrivingSystem:
def __init__(self, selfdrive):
self.mads_params = MadsParams()
self.params = selfdrive.params
self.enabled = False
self.active = False
@@ -60,14 +40,14 @@ class ModularAssistiveDrivingSystem:
self.allow_always = True
# read params on init
self.enabled_toggle = self.mads_params.read_param("Mads")
self.main_enabled_toggle = self.mads_params.read_param("MadsMainCruiseAllowed")
self.pause_lateral_on_brake_toggle = self.mads_params.read_param("MadsPauseLateralOnBrake")
self.unified_engagement_mode = self.mads_params.read_param("MadsUnifiedEngagementMode")
self.enabled_toggle = self.params.get_bool("Mads")
self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed")
self.steering_mode_on_brake = read_steering_mode_param(self.params)
self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode")
def read_params(self):
self.main_enabled_toggle = self.mads_params.read_param("MadsMainCruiseAllowed")
self.unified_engagement_mode = self.mads_params.read_param("MadsUnifiedEngagementMode")
self.main_enabled_toggle = self.params.get_bool("MadsMainCruiseAllowed")
self.unified_engagement_mode = self.params.get_bool("MadsUnifiedEngagementMode")
def update_events(self, CS: car.CarState):
def update_unified_engagement_mode():
@@ -104,11 +84,11 @@ class ModularAssistiveDrivingSystem:
replace_event(EventName.parkBrake, EventNameSP.silentParkBrake)
transition_paused_state()
if self.pause_lateral_on_brake_toggle:
if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE:
if CS.brakePressed:
transition_paused_state()
if not (self.pause_lateral_on_brake_toggle and CS.brakePressed) and \
if not (self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and CS.brakePressed) and \
not self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT):
if self.state_machine.state == State.paused:
self.events_sp.add(EventNameSP.silentLkasEnable)
@@ -144,6 +124,12 @@ class ModularAssistiveDrivingSystem:
if self.selfdrive.CS_prev.cruiseState.available:
self.events_sp.add(EventNameSP.lkasDisable)
if self.steering_mode_on_brake == MadsSteeringModeOnBrake.DISENGAGE:
# Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0
if (CS.brakePressed and (not self.selfdrive.CS_prev.brakePressed or not CS.standstill)) or \
(CS.regenBraking and (not self.selfdrive.CS_prev.regenBraking or not CS.standstill)):
self.events_sp.add(EventNameSP.lkasDisable)
self.events.remove(EventName.pcmDisable)
self.events.remove(EventName.buttonCancel)
self.events.remove(EventName.pedalPressed)
+2 -21
View File
@@ -1,27 +1,8 @@
"""
The MIT License
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Last updated: July 29, 2024
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import log, custom
@@ -1,27 +1,8 @@
"""
The MIT License
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Last updated: July 29, 2024
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pytest
+6 -4
View File
@@ -1,7 +1,9 @@
# Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
#
# This file is part of sunnypilot and is licensed under the MIT License.
# See the LICENSE.md file in the root directory for more details.
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import os
import pickle
+6 -4
View File
@@ -1,7 +1,9 @@
# Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
#
# This file is part of sunnypilot and is licensed under the MIT License.
# See the LICENSE.md file in the root directory for more details.
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import time
+6 -4
View File
@@ -1,7 +1,9 @@
# Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
#
# This file is part of sunnypilot and is licensed under the MIT License.
# See the LICENSE.md file in the root directory for more details.
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import hashlib
import os
+6 -4
View File
@@ -1,7 +1,9 @@
# Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
#
# This file is part of sunnypilot and is licensed under the MIT License.
# See the LICENSE.md file in the root directory for more details.
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import asyncio
import os
@@ -1,7 +1,9 @@
# Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
#
# This file is part of sunnypilot and is licensed under the MIT License.
# See the LICENSE.md file in the root directory for more details.
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import messaging, custom
+1 -1
View File
@@ -55,7 +55,7 @@ class SunnylinkApi(BaseApi):
imei_try = 0
while imei1 is None and imei2 is None and imei_try < MAX_RETRIES:
try:
imei1, imei2 = self.params.get("IMEI", encoding='utf8') or HARDWARE.get_imei(0), HARDWARE.get_imei(1)
imei1, imei2 = 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)
+6 -3
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import base64
import gzip
import os
import ssl
import threading
import time
@@ -29,7 +30,8 @@ SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require
DISALLOW_LOG_UPLOAD = threading.Event()
params = Params()
sunnylink_api = SunnylinkApi(params.get("SunnylinkDongleId", encoding='utf-8'))
sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8')
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
@@ -211,7 +213,7 @@ def main(exit_event: threading.Event = None):
UploadQueueCache.initialize(upload_queue)
ws_uri = SUNNYLINK_ATHENA_HOST
ws_uri = f"{SUNNYLINK_ATHENA_HOST}"
conn_start = None
conn_retries = 0
while (exit_event is None or not exit_event.is_set()) and sunnylink_ready(params):
@@ -222,8 +224,9 @@ def main(exit_event: threading.Event = None):
cloudlog.event("sunnylinkd.main.connecting_ws", ws_uri=ws_uri, retries=conn_retries)
ws = create_connection(
ws_uri,
cookie=f"jwt={sunnylink_api.get_token()}",
header={"Authorization": f"Bearer {sunnylink_api.get_token()}"},
enable_multithread=True,
sslopt={"cert_reqs": ssl.CERT_NONE if "localhost" in ws_uri else ssl.CERT_REQUIRED},
timeout=SUNNYLINK_RECONNECT_TIMEOUT_S,
)
cloudlog.event("sunnylinkd.main.connected_ws", ws_uri=ws_uri, retries=conn_retries,
+2 -1
View File
@@ -141,7 +141,8 @@ void CameraState::set_camera_exposure(float grey_frac) {
// TODO: Lower latency to 2 frames, by using the histogram outputted by the sensor we can do AE before the debayering is complete
const auto &sensor = camera.sensor;
const float cur_ev_ = cur_ev[camera.buf.cur_frame_data.frame_id % 3] * sensor->ev_scale;
// Offset idx by one to not get stuck in self loop
const float cur_ev_ = cur_ev[(camera.buf.cur_frame_data.frame_id - 1) % 3] * sensor->ev_scale;
// Scale target grey between min and 0.4 depending on lighting conditions
float new_target_grey = std::clamp(0.4 - 0.3 * log2(1.0 + sensor->target_grey_factor*cur_ev_) / log2(6000.0), target_grey_minimums[camera.cc.camera_num], 0.4);
+27 -5
View File
@@ -237,7 +237,7 @@ SpectraCamera::SpectraCamera(SpectraMaster *master, const CameraConfig &config)
: m(master),
enabled(config.enabled),
cc(config) {
ife_buf_depth = (cc.output_type == ISP_RAW_OUTPUT) ? 4 : VIPC_BUFFER_COUNT;
ife_buf_depth = VIPC_BUFFER_COUNT;
assert(ife_buf_depth < MAX_IFE_BUFS);
}
@@ -266,7 +266,7 @@ int SpectraCamera::clear_req_queue() {
req_mgr_flush_request.link_hdl = link_handle;
req_mgr_flush_request.flush_type = CAM_REQ_MGR_FLUSH_TYPE_ALL;
int ret = do_cam_control(m->video0_fd, CAM_REQ_MGR_FLUSH_REQ, &req_mgr_flush_request, sizeof(req_mgr_flush_request));
LOGD("flushed all req: %d", ret);
LOGD("flushed all req: %d", ret); // returns a "time until timeout" on clearing the workq
for (int i = 0; i < MAX_IFE_BUFS; ++i) {
destroySyncObjectAt(i);
@@ -1258,6 +1258,8 @@ void SpectraCamera::camera_close() {
LOG("-- Stop devices %d", cc.camera_num);
if (enabled) {
clear_req_queue();
// ret = device_control(sensor_fd, CAM_STOP_DEV, session_handle, sensor_dev_handle);
// LOGD("stop sensor: %d", ret);
int ret = device_control(m->isp_fd, CAM_STOP_DEV, session_handle, isp_dev_handle);
@@ -1328,7 +1330,16 @@ bool SpectraCamera::handle_camera_event(const cam_req_mgr_message *event_data) {
uint64_t timestamp = event_data->u.frame_msg.timestamp; // timestamped in the kernel's SOF IRQ callback
//LOGD("handle cam %d ts %lu req id %lu frame id %lu", cc.camera_num, timestamp, request_id, frame_id_raw);
if (stress_test("skipping SOF event")) return false;
// if there's a lag, some more frames could have already come in before
// we cleared the queue, so we'll still get them with valid (> 0) request IDs.
if (timestamp < last_requeue_ts) {
LOGD("skipping frame: ts before requeue / cam %d ts %lu req id %lu frame id %lu", cc.camera_num, timestamp, request_id, frame_id_raw);
return false;
}
if (stress_test("skipping SOF event")) {
return false;
}
if (!validateEvent(request_id, frame_id_raw)) {
return false;
@@ -1379,7 +1390,7 @@ bool SpectraCamera::validateEvent(uint64_t request_id, uint64_t frame_id_raw) {
if (request_id != request_id_last + 1) {
LOGE("camera %d requests skipped %ld -> %ld", cc.camera_num, request_id_last, request_id);
clearAndRequeue(request_id_last + 1);
clearAndRequeue(request_id + 1);
return false;
}
}
@@ -1390,6 +1401,7 @@ void SpectraCamera::clearAndRequeue(uint64_t from_request_id) {
// clear everything, then queue up a fresh set of frames
LOGW("clearing and requeuing camera %d from %lu", cc.camera_num, from_request_id);
clear_req_queue();
last_requeue_ts = nanos_since_boot();
for (uint64_t id = from_request_id; id < from_request_id + ife_buf_depth; ++id) {
enqueue_frame(id);
}
@@ -1400,11 +1412,20 @@ bool SpectraCamera::waitForFrameReady(uint64_t request_id) {
int buf_idx = request_id % ife_buf_depth;
assert(sync_objs_ife[buf_idx]);
if (stress_test("sync sleep time")) {
util::sleep_for(350);
return false;
}
auto waitForSync = [&](uint32_t sync_obj, int timeout_ms, const char *sync_type) {
double st = millis_since_boot();
struct cam_sync_wait sync_wait = {};
sync_wait.sync_obj = sync_obj;
sync_wait.timeout_ms = stress_test(sync_type) ? 1 : timeout_ms;
return do_sync_control(m->cam_sync_fd, CAM_SYNC_WAIT, &sync_wait, sizeof(sync_wait)) == 0;
bool ret = do_sync_control(m->cam_sync_fd, CAM_SYNC_WAIT, &sync_wait, sizeof(sync_wait)) == 0;
double et = millis_since_boot();
if (!ret) LOGE("camera %d %s failed after %.2fms", cc.camera_num, sync_type, et-st);
return ret;
};
// wait for frame from IFE
@@ -1415,6 +1436,7 @@ bool SpectraCamera::waitForFrameReady(uint64_t request_id) {
// BPS is typically 7ms
success = waitForSync(sync_objs_bps[buf_idx], 50, "BPS sync");
}
return success;
}
+10 -4
View File
@@ -181,6 +181,7 @@ public:
int sync_objs_ife[MAX_IFE_BUFS] = {};
int sync_objs_bps[MAX_IFE_BUFS] = {};
uint64_t request_id_last = 0;
uint64_t last_requeue_ts = 0;
uint64_t frame_id_raw_last = 0;
int invalid_request_count = 0;
bool skip_expected = true;
@@ -202,11 +203,16 @@ private:
inline static bool first_frame_synced = false;
// a mode for stressing edge cases: realignment, sync failures, etc.
inline bool stress_test(const char* log) {
static double prob = std::stod(util::getenv("SPECTRA_ERROR_PROB", "-1"));;
bool triggered = (prob > 0) && ((static_cast<double>(rand()) / RAND_MAX) < prob);
inline bool stress_test(std::string log) {
static double last_trigger = 0;
static double prob = std::stod(util::getenv("SPECTRA_ERROR_PROB", "-1"));
static double dt = std::stod(util::getenv("SPECTRA_ERROR_DT", "1"));
bool triggered = (prob > 0) && \
((static_cast<double>(rand()) / RAND_MAX) < prob) && \
(millis_since_boot() - last_trigger) > dt;
if (triggered) {
LOGE("stress test (cam %d): %s", cc.camera_num, log);
last_trigger = millis_since_boot();
LOGE("stress test (cam %d): %s", cc.camera_num, log.c_str());
}
return triggered;
}
+2 -3
View File
@@ -6,9 +6,8 @@
#include "common/util.h"
int main(int argc, char *argv[]) {
int ret = util::set_realtime_priority(53);
assert(ret == 0);
ret = util::set_core_affinity({6});
// doesn't need RT priority since we're using isolcpus
int ret = util::set_core_affinity({6});
assert(ret == 0 || Params().getBool("IsOffroad")); // failure ok while offroad due to offlining cores
camerad_thread();
+11 -11
View File
@@ -56,28 +56,28 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-bca7573652def58a0afc40bbdd550d63dc08ed2e925ace69032aef84bb9dc4ba.img.xz",
"hash": "bca7573652def58a0afc40bbdd550d63dc08ed2e925ace69032aef84bb9dc4ba",
"hash_raw": "bca7573652def58a0afc40bbdd550d63dc08ed2e925ace69032aef84bb9dc4ba",
"size": 18475008,
"url": "https://commadist.azureedge.net/agnosupdate/boot-0d0d4d5a32e00b46fa36180b4a96337f2a53302d8bd0faee95f8fe1063d1e24a.img.xz",
"hash": "0d0d4d5a32e00b46fa36180b4a96337f2a53302d8bd0faee95f8fe1063d1e24a",
"hash_raw": "0d0d4d5a32e00b46fa36180b4a96337f2a53302d8bd0faee95f8fe1063d1e24a",
"size": 18479104,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "8907415564e8a242548e871b534dcd53240fe4e4517700c6c85b5637e365f0b0"
"ondevice_hash": "47b2096995578a5078e393c33108b42756009dbb361c43c508fc93cd8bda99cc"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-5612484e7f255659c0845de620e7c733afd2e1b939f9464f5ef039721bb7cba9.img.xz",
"hash": "e4872f4132111b7b28586d978dd01bb48ffa031e103d029ebede7613c1bc2aa6",
"hash_raw": "5612484e7f255659c0845de620e7c733afd2e1b939f9464f5ef039721bb7cba9",
"url": "https://commadist.azureedge.net/agnosupdate/system-ff2da7bc34a1ad8e2831a13a49685243957a51b762d8f28e14e98ac90e84d211.img.xz",
"hash": "fbda983c40e75d8c4784a45c687f27dd27f97f7f0e9a71b4981ec2092ff68a72",
"hash_raw": "ff2da7bc34a1ad8e2831a13a49685243957a51b762d8f28e14e98ac90e84d211",
"size": 4404019200,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "4e5e680b4ac387ddc974b32dd3d5ec1d76282511eab974866b3b72399034985e",
"ondevice_hash": "428c744458d5f35199aab96a0928876c9deeff3e753e95044291408ba244f0af",
"alt": {
"hash": "5612484e7f255659c0845de620e7c733afd2e1b939f9464f5ef039721bb7cba9",
"url": "https://commadist.azureedge.net/agnosupdate/system-5612484e7f255659c0845de620e7c733afd2e1b939f9464f5ef039721bb7cba9.img",
"hash": "ff2da7bc34a1ad8e2831a13a49685243957a51b762d8f28e14e98ac90e84d211",
"url": "https://commadist.azureedge.net/agnosupdate/system-ff2da7bc34a1ad8e2831a13a49685243957a51b762d8f28e14e98ac90e84d211.img",
"size": 4404019200
}
}

Some files were not shown because too many files have changed in this diff Show More