mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-25 11:52:20 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35a616c235 | |||
| a2f25897d2 |
@@ -1,77 +0,0 @@
|
||||
name: cereal validation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- master-new
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cereal/**'
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
run_number:
|
||||
default: '1'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
PYTHONWARNINGS: error
|
||||
BASE_IMAGE: openpilot-base
|
||||
BUILD: selfdrive/test/docker_build.sh base
|
||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||
|
||||
jobs:
|
||||
generate_cereal_artifact:
|
||||
name: Generate cereal validation artifacts
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: ./.github/workflows/setup-with-retry
|
||||
- name: Build openpilot
|
||||
run: ${{ env.RUN }} "scons -j$(nproc) cereal"
|
||||
- name: Generate the log file
|
||||
run: |
|
||||
${{ env.RUN }} "cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema_instances.bin" && \
|
||||
ls -la
|
||||
ls -la cereal/messaging/tests
|
||||
- name: 'Prepare artifact'
|
||||
run: |
|
||||
mkdir -p "cereal/messaging/tests/cereal_validations"
|
||||
cp cereal/messaging/tests/validate_sp_cereal_upstream.py "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py"
|
||||
cp schema_instances.bin "cereal/messaging/tests/cereal_validations/schema_instances.bin"
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cereal_validations
|
||||
path: cereal/messaging/tests/cereal_validations
|
||||
|
||||
validate_cereal_with_upstream:
|
||||
name: Validate cereal with Upstream
|
||||
runs-on: ubuntu-24.04
|
||||
needs: generate_cereal_artifact
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'commaai/openpilot'
|
||||
submodules: true
|
||||
ref: "refs/heads/master"
|
||||
- uses: ./.github/workflows/setup-with-retry
|
||||
- name: Build openpilot
|
||||
run: ${{ env.RUN }} "scons -j$(nproc) cereal"
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cereal_validations
|
||||
path: cereal/messaging/tests/cereal_validations
|
||||
- name: 'Run the validation'
|
||||
run: |
|
||||
chmod +x cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py
|
||||
${{ env.RUN }} "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f cereal/messaging/tests/cereal_validations/schema_instances.bin"
|
||||
@@ -38,7 +38,6 @@ jobs:
|
||||
new_branch: ${{ steps.set-env.outputs.new_branch }}
|
||||
version: ${{ steps.set-env.outputs.version }}
|
||||
extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }}
|
||||
commit_sha: ${{ steps.set-env.outputs.commit_sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -95,7 +94,6 @@ jobs:
|
||||
echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT
|
||||
[[ ! -z "$EXTRA_VERSION_IDENTIFIER" ]] && echo "extra_version_identifier=$EXTRA_VERSION_IDENTIFIER" >> $GITHUB_OUTPUT
|
||||
[[ ! -z "$VERSION" ]] && echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Set up common environment
|
||||
source /etc/profile;
|
||||
|
||||
@@ -4,8 +4,6 @@ env:
|
||||
DEFAULT_SOURCE_BRANCH: "master-new"
|
||||
DEFAULT_TARGET_BRANCH: "nightly"
|
||||
PR_LABEL: "dev-c3"
|
||||
LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs'
|
||||
LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -37,15 +35,6 @@ jobs:
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Set up SSH
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Add GitLab public keys
|
||||
run: |
|
||||
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -63,10 +52,10 @@ jobs:
|
||||
echo "Source branch ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }} does not exist!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Make sure we have the latest source branch
|
||||
git fetch origin ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}
|
||||
|
||||
|
||||
# Check if target branch exists
|
||||
if ! git ls-remote --heads origin ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} | grep -q "${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"; then
|
||||
echo "Target branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} does not exist, creating it from ${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}"
|
||||
@@ -110,29 +99,20 @@ jobs:
|
||||
}
|
||||
}
|
||||
}' -F label="is:pr is:open label:${PR_LABEL} sort:created-asc")
|
||||
|
||||
|
||||
echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Process PRs
|
||||
run: |
|
||||
cp ${{ github.workspace }}/release/ci/squash_and_merge.py /tmp/squash_and_merge.py && \
|
||||
chmod +x /tmp/squash_and_merge.py && \
|
||||
python3 ${{ github.workspace }}/release/ci/squash_and_merge_prs.py \
|
||||
--pr-data '${{ steps.get-prs.outputs.PR_LIST }}' \
|
||||
--target-branch ${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }} \
|
||||
--squash-script-path '/tmp/squash_and_merge.py'
|
||||
--squash-script-path '${{ github.workspace }}/release/ci/squash_and_merge.py'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update LFS Config
|
||||
run: |
|
||||
echo '[lfs]' > .lfsconfig
|
||||
echo ' url = ${{ env.LFS_URL }}' >> .lfsconfig
|
||||
echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig
|
||||
echo ' locksverify = false' >> .lfsconfig
|
||||
|
||||
- name: Push changes if there are diffs
|
||||
id: push-changes # Add an id so we can reference this step
|
||||
run: |
|
||||
|
||||
+13
-66
@@ -10,13 +10,6 @@ $Cxx.namespace("cereal");
|
||||
# DO rename the structs
|
||||
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
|
||||
|
||||
enum LongitudinalPersonalitySP {
|
||||
aggressive @0;
|
||||
standard @1;
|
||||
relaxed @2;
|
||||
overtake @3;
|
||||
}
|
||||
|
||||
struct ModularAssistiveDrivingSystem {
|
||||
state @0 :ModularAssistiveDrivingSystemState;
|
||||
enabled @1 :Bool;
|
||||
@@ -34,7 +27,6 @@ struct ModularAssistiveDrivingSystem {
|
||||
|
||||
struct SelfdriveStateSP @0x81c2f05a394cf4af {
|
||||
mads @0 :ModularAssistiveDrivingSystem;
|
||||
personality @1 :LongitudinalPersonalitySP;
|
||||
}
|
||||
|
||||
struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
@@ -97,8 +89,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
dec @0 :DynamicExperimentalControl;
|
||||
|
||||
personalityDEPRECATED @1 :LongitudinalPersonalitySP;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
enabled @1 :Bool;
|
||||
@@ -112,23 +102,19 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
}
|
||||
|
||||
struct OnroadEventSP @0xda96579883444c35 {
|
||||
events @0 :List(Event);
|
||||
name @0 :EventName;
|
||||
|
||||
struct Event {
|
||||
name @0 :EventName;
|
||||
|
||||
# event types
|
||||
enable @1 :Bool;
|
||||
noEntry @2 :Bool;
|
||||
warning @3 :Bool; # alerts presented only when enabled or soft disabling
|
||||
userDisable @4 :Bool;
|
||||
softDisable @5 :Bool;
|
||||
immediateDisable @6 :Bool;
|
||||
preEnable @7 :Bool;
|
||||
permanent @8 :Bool; # alerts presented regardless of openpilot state
|
||||
overrideLateral @10 :Bool;
|
||||
overrideLongitudinal @9 :Bool;
|
||||
}
|
||||
# event types
|
||||
enable @1 :Bool;
|
||||
noEntry @2 :Bool;
|
||||
warning @3 :Bool; # alerts presented only when enabled or soft disabling
|
||||
userDisable @4 :Bool;
|
||||
softDisable @5 :Bool;
|
||||
immediateDisable @6 :Bool;
|
||||
preEnable @7 :Bool;
|
||||
permanent @8 :Bool; # alerts presented regardless of openpilot state
|
||||
overrideLateral @10 :Bool;
|
||||
overrideLongitudinal @9 :Bool;
|
||||
|
||||
enum EventName {
|
||||
lkasEnable @0;
|
||||
@@ -170,46 +156,7 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
||||
mads @0 :ModularAssistiveDrivingSystem;
|
||||
}
|
||||
|
||||
struct BackupManagerSP @0xf98d843bfd7004a3 {
|
||||
backupStatus @0 :Status;
|
||||
restoreStatus @1 :Status;
|
||||
backupProgress @2 :Float32;
|
||||
restoreProgress @3 :Float32;
|
||||
lastError @4 :Text;
|
||||
currentBackup @5 :BackupInfo;
|
||||
backupHistory @6 :List(BackupInfo);
|
||||
|
||||
enum Status {
|
||||
idle @0;
|
||||
inProgress @1;
|
||||
completed @2;
|
||||
failed @3;
|
||||
}
|
||||
|
||||
struct Version {
|
||||
major @0 :UInt16;
|
||||
minor @1 :UInt16;
|
||||
patch @2 :UInt16;
|
||||
build @3 :UInt16;
|
||||
branch @4 :Text;
|
||||
}
|
||||
|
||||
struct MetadataEntry {
|
||||
key @0 :Text;
|
||||
value @1 :Text;
|
||||
tags @2 :List(Text);
|
||||
}
|
||||
|
||||
struct BackupInfo {
|
||||
deviceId @0 :Text;
|
||||
version @1 :UInt32;
|
||||
config @2 :Text;
|
||||
isEncrypted @3 :Bool;
|
||||
createdAt @4 :Text; # ISO timestamp
|
||||
updatedAt @5 :Text; # ISO timestamp
|
||||
sunnypilotVersion @6 :Version;
|
||||
backupMetadata @7 :List(MetadataEntry);
|
||||
}
|
||||
struct CustomReserved6 @0xf98d843bfd7004a3 {
|
||||
}
|
||||
|
||||
struct CustomReserved7 @0xb86e6369214c01c8 {
|
||||
|
||||
+2
-2
@@ -2579,10 +2579,10 @@ struct Event {
|
||||
selfdriveStateSP @107 :Custom.SelfdriveStateSP;
|
||||
modelManagerSP @108 :Custom.ModelManagerSP;
|
||||
longitudinalPlanSP @109 :Custom.LongitudinalPlanSP;
|
||||
onroadEventsSP @110 :Custom.OnroadEventSP;
|
||||
onroadEventsSP @110 :List(Custom.OnroadEventSP);
|
||||
carParamsSP @111 :Custom.CarParamsSP;
|
||||
carControlSP @112 :Custom.CarControlSP;
|
||||
backupManagerSP @113 :Custom.BackupManagerSP;
|
||||
customReserved6 @113 :Custom.CustomReserved6;
|
||||
customReserved7 @114 :Custom.CustomReserved7;
|
||||
customReserved8 @115 :Custom.CustomReserved8;
|
||||
customReserved9 @116 :Custom.CustomReserved9;
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def print_debug(string: str) -> None:
|
||||
if DEBUG:
|
||||
print(string)
|
||||
|
||||
|
||||
def create_schema_instance(struct: Any, prop: Tuple[str, Any]) -> Any:
|
||||
"""
|
||||
Create a new instance of a schema type, handling different field types.
|
||||
|
||||
Args:
|
||||
struct: The Cap'n Proto schema structure
|
||||
prop: A tuple containing the field name and field metadata
|
||||
|
||||
Returns:
|
||||
A new initialized schema instance
|
||||
"""
|
||||
struct_instance = struct.new_message()
|
||||
field_name, field_metadata = prop
|
||||
|
||||
try:
|
||||
field_type = field_metadata.proto.slot.type.which()
|
||||
|
||||
# Initialize different types of fields
|
||||
if field_type in ('list', 'text', 'data'):
|
||||
struct_instance.init(field_name, 1)
|
||||
print_debug(f"Initialized list/text/data field: {field_name}")
|
||||
elif field_type in ('struct', 'object'):
|
||||
struct_instance.init(field_name)
|
||||
print_debug(f"Initialized struct/object field: {field_name}")
|
||||
|
||||
return struct_instance
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating instance for {field_name}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_schema_fields(schema_struct: Any) -> List[Tuple[str, Any]]:
|
||||
"""
|
||||
Retrieve all fields from a given schema structure.
|
||||
|
||||
Args:
|
||||
schema_struct: The Cap'n Proto schema structure
|
||||
|
||||
Returns:
|
||||
A list of field names and their metadata
|
||||
"""
|
||||
try:
|
||||
# Get all fields from the schema
|
||||
schema_fields = list(schema_struct.schema.fields.items())
|
||||
|
||||
print_debug("Discovered schema fields:")
|
||||
for field_name, field_metadata in schema_fields:
|
||||
print_debug(f"- {field_name}")
|
||||
|
||||
return schema_fields
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving schema fields: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def generate_schema_instances(schema_struct: Any) -> List[Any]:
|
||||
"""
|
||||
Generate instances for all fields in a given schema.
|
||||
|
||||
Args:
|
||||
schema_struct: The Cap'n Proto schema structure
|
||||
|
||||
Returns:
|
||||
A list of schema instances
|
||||
"""
|
||||
schema_fields = get_schema_fields(schema_struct)
|
||||
instances = []
|
||||
|
||||
for field_prop in schema_fields:
|
||||
try:
|
||||
instance = create_schema_instance(schema_struct, field_prop)
|
||||
if instance is not None:
|
||||
instances.append(instance)
|
||||
except Exception as e:
|
||||
print(f"Skipping field due to error: {e}")
|
||||
|
||||
print(f"Generated {len(instances)} schema instances")
|
||||
return instances
|
||||
|
||||
|
||||
def persist_instances(instances: List[Any], filename: str) -> None:
|
||||
"""
|
||||
Write schema instances to a binary file.
|
||||
|
||||
Args:
|
||||
instances: List of schema instances
|
||||
filename: Output file path
|
||||
"""
|
||||
try:
|
||||
with open(filename, 'wb') as f:
|
||||
for instance in instances:
|
||||
f.write(instance.to_bytes())
|
||||
|
||||
print(f"Successfully wrote {len(instances)} instances to {filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error persisting instances: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def read_instances(filename: str, schema_type: Any) -> List[Any]:
|
||||
"""
|
||||
Read schema instances from a binary file.
|
||||
|
||||
Args:
|
||||
filename: Input file path
|
||||
schema_type: The schema type to use for reading
|
||||
|
||||
Returns:
|
||||
A list of read schema instances
|
||||
"""
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
instances = list(schema_type.read_multiple_bytes(data))
|
||||
|
||||
print(f"Read {len(instances)} instances from {filename}")
|
||||
return instances
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading instances: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def compare_schemas(original_instances: List[Any], read_instances: List[Any]) -> bool:
|
||||
"""
|
||||
Compare original and read-back instances to detect potential breaking changes.
|
||||
|
||||
Args:
|
||||
original_instances: List of originally generated instances
|
||||
read_instances: List of instances read back from file
|
||||
|
||||
Returns:
|
||||
Boolean indicating whether schemas appear compatible
|
||||
"""
|
||||
if len(original_instances) != len(read_instances):
|
||||
print("❌ Schema Compatibility Warning: Instance count mismatch")
|
||||
return False
|
||||
|
||||
compatible = True
|
||||
for struct in read_instances:
|
||||
try:
|
||||
getattr(struct, struct.which()) # Attempting to access the field to validate readability
|
||||
except Exception as e:
|
||||
print(f"❌ Structural change detected: {struct.which()} is not readable.\nFull error: {e}")
|
||||
compatible = False
|
||||
|
||||
return compatible
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
CLI entry point for schema compatibility testing.
|
||||
"""
|
||||
# Setup argument parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Cap\'n Proto Schema Compatibility Testing Tool',
|
||||
epilog='Test schema compatibility by generating and reading back instances.'
|
||||
)
|
||||
|
||||
# Add mutually exclusive group for generation or reading mode
|
||||
mode_group = parser.add_mutually_exclusive_group(required=True)
|
||||
mode_group.add_argument('-g', '--generate', action='store_true',
|
||||
help='Generate schema instances')
|
||||
mode_group.add_argument('-r', '--read', action='store_true',
|
||||
help='Read and validate schema instances')
|
||||
|
||||
# Common arguments
|
||||
parser.add_argument('-f', '--file',
|
||||
default='schema_instances.bin',
|
||||
help='Output/input binary file (default: schema_instances.bin)')
|
||||
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
# Import the schema dynamically
|
||||
try:
|
||||
from cereal import log
|
||||
schema_type = log.Event
|
||||
except ImportError:
|
||||
print("Error: Unable to import schema. Ensure 'cereal' is installed.")
|
||||
sys.exit(1)
|
||||
|
||||
# Execute based on mode
|
||||
if args.generate:
|
||||
print("🔧 Generating Schema Instances")
|
||||
instances = generate_schema_instances(schema_type)
|
||||
persist_instances(instances, args.file)
|
||||
print("✅ Instance generation complete")
|
||||
|
||||
elif args.read:
|
||||
print("🔍 Reading and Validating Schema Instances")
|
||||
generated_instances = generate_schema_instances(schema_type)
|
||||
read_back_instances = read_instances(args.file, schema_type)
|
||||
|
||||
# Compare schemas
|
||||
if compare_schemas(generated_instances, read_back_instances):
|
||||
print("✅ Schema Compatibility: No breaking changes detected")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("❌ Potential Schema Breaking Changes Detected")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -76,7 +76,6 @@ _services: dict[str, tuple] = {
|
||||
|
||||
# sunnypilot
|
||||
"modelManagerSP": (False, 1., 1),
|
||||
"backupManagerSP": (False, 1., 1),
|
||||
"selfdriveStateSP": (True, 100., 10),
|
||||
"longitudinalPlanSP": (True, 20., 10),
|
||||
"onroadEventsSP": (True, 1., 1),
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ class BaseApi:
|
||||
ascii_encoded_text = normalized_text.encode('ascii', 'ignore')
|
||||
return ascii_encoded_text.decode()
|
||||
|
||||
def api_get(self, endpoint, method='GET', timeout=None, access_token=None, json=None, **params):
|
||||
def api_get(self, endpoint, method='GET', timeout=None, access_token=None, **params):
|
||||
headers = {}
|
||||
if access_token is not None:
|
||||
headers['Authorization'] = "JWT " + access_token
|
||||
@@ -53,4 +53,4 @@ class BaseApi:
|
||||
version = self.remove_non_ascii_chars(get_version())
|
||||
headers['User-Agent'] = self.user_agent + version
|
||||
|
||||
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
|
||||
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, params=params)
|
||||
|
||||
+2
-4
@@ -103,12 +103,10 @@ Params::~Params() {
|
||||
assert(queue.empty());
|
||||
}
|
||||
|
||||
std::vector<std::string> Params::allKeys(ParamKeyType type) const {
|
||||
std::vector<std::string> Params::allKeys() const {
|
||||
std::vector<std::string> ret;
|
||||
for (auto &p : keys) {
|
||||
if (type == ALL || (p.second & type)) {
|
||||
ret.push_back(p.first);
|
||||
}
|
||||
ret.push_back(p.first);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public:
|
||||
Params(const Params&) = delete;
|
||||
Params& operator=(const Params&) = delete;
|
||||
|
||||
std::vector<std::string> allKeys(ParamKeyType type = ALL) const;
|
||||
std::vector<std::string> allKeys() const;
|
||||
bool checkKey(const std::string &key);
|
||||
ParamKeyType getKeyType(const std::string &key);
|
||||
inline std::string getParamPath(const std::string &key = {}) {
|
||||
|
||||
@@ -120,8 +120,6 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"ApiCache_DriveStats", PERSISTENT},
|
||||
{"AutoLaneChangeBsmDelay", PERSISTENT},
|
||||
{"AutoLaneChangeTimer", PERSISTENT},
|
||||
{"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"CarParamsSPCache", CLEAR_ON_MANAGER_START},
|
||||
{"CarParamsSPPersistent", PERSISTENT},
|
||||
@@ -130,7 +128,6 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"OffroadMode", CLEAR_ON_MANAGER_START},
|
||||
{"OffroadMode_Status", CLEAR_ON_MANAGER_START},
|
||||
{"QuietMode", PERSISTENT | BACKUP},
|
||||
|
||||
// MADS params
|
||||
{"Mads", PERSISTENT | BACKUP},
|
||||
@@ -156,10 +153,6 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"SunnylinkdPid", PERSISTENT},
|
||||
{"SunnylinkEnabled", PERSISTENT},
|
||||
|
||||
// Backup Manager params
|
||||
{"BackupManager_CreateBackup", PERSISTENT},
|
||||
{"BackupManager_RestoreVersion", PERSISTENT},
|
||||
|
||||
// sunnypilot car specific params
|
||||
{"HyundaiRadarTracks", PERSISTENT},
|
||||
{"HyundaiRadarTracksConfirmed", PERSISTENT},
|
||||
|
||||
@@ -11,7 +11,6 @@ cdef extern from "common/params.h":
|
||||
CLEAR_ON_ONROAD_TRANSITION
|
||||
CLEAR_ON_OFFROAD_TRANSITION
|
||||
DEVELOPMENT_ONLY
|
||||
BACKUP
|
||||
ALL
|
||||
|
||||
cdef cppclass c_Params "Params":
|
||||
@@ -26,7 +25,7 @@ cdef extern from "common/params.h":
|
||||
bool checkKey(string) nogil
|
||||
string getParamPath(string) nogil
|
||||
void clearAll(ParamKeyType)
|
||||
vector[string] allKeys(ParamKeyType)
|
||||
vector[string] allKeys()
|
||||
|
||||
|
||||
def ensure_bytes(v):
|
||||
@@ -120,5 +119,5 @@ cdef class Params:
|
||||
cdef string key_bytes = ensure_bytes(key)
|
||||
return self.p.getParamPath(key_bytes).decode("utf-8")
|
||||
|
||||
def all_keys(self, type=ParamKeyType.ALL):
|
||||
return self.p.allKeys(type)
|
||||
def all_keys(self):
|
||||
return self.p.allKeys()
|
||||
|
||||
+344
-37
@@ -1,53 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
import shutil
|
||||
import signal
|
||||
import contextlib
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
def run_git_command(command, check=True):
|
||||
def run_command(command: str) -> tuple[int, str, str]:
|
||||
"""Run a shell command and return exit code, stdout, and stderr."""
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
stdout, stderr = process.communicate()
|
||||
return process.returncode, stdout.strip(), stderr.strip()
|
||||
|
||||
|
||||
def is_gh_available() -> bool:
|
||||
"""Check if GitHub CLI is available."""
|
||||
return shutil.which('gh') is not None
|
||||
|
||||
|
||||
def get_current_branch() -> str | None:
|
||||
"""Get the name of the current git branch."""
|
||||
code, output, error = run_command("git rev-parse --abbrev-ref HEAD")
|
||||
if code != 0:
|
||||
print(f"Error getting current branch: {error}")
|
||||
return None
|
||||
return output
|
||||
|
||||
|
||||
def backup_branch(branch_name: str) -> bool:
|
||||
"""Create a backup of the current branch."""
|
||||
backup_name = f"{branch_name}-backup-$(date +%Y%m%d_%H%M%S)"
|
||||
code, _, error = run_command(f"git branch {backup_name}")
|
||||
if code != 0:
|
||||
print(f"Error creating backup branch: {error}")
|
||||
return False
|
||||
print(f"Created backup branch: {backup_name}")
|
||||
return True
|
||||
|
||||
|
||||
def get_commit_messages(source_branch: str, target_branch: str) -> list[str] | None:
|
||||
"""Get all commit messages between source and target branches."""
|
||||
code, output, error = run_command(f"git log {target_branch}..{source_branch} --format=%B")
|
||||
if code != 0:
|
||||
print(f"Error getting commit messages: {error}")
|
||||
return None
|
||||
return [msg.strip() for msg in output.splitlines() if msg and not msg.startswith('Merge')]
|
||||
|
||||
|
||||
def get_pr_info(branch_name: str) -> str | None:
|
||||
"""Get PR title using GitHub CLI."""
|
||||
if not is_gh_available():
|
||||
print("Warning: GitHub CLI not found. Install it to auto-fetch PR titles:")
|
||||
print(" https://cli.github.com/")
|
||||
return None
|
||||
|
||||
# Try to get PR info using gh cli
|
||||
code, output, error = run_command(f"gh pr view --json title --jq .title {branch_name}")
|
||||
if code != 0:
|
||||
print(f"No open PR found for branch '{branch_name}'")
|
||||
return None
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def create_squash_message(pr_title: str | None, commit_messages: list[str], source_branch: str) -> str:
|
||||
"""Create a squash commit message from PR title and commit messages."""
|
||||
parts = []
|
||||
|
||||
# Add PR title if provided
|
||||
if pr_title:
|
||||
parts.append(pr_title)
|
||||
else:
|
||||
parts.append(f"Squashed changes from {source_branch}")
|
||||
parts.append("") # Empty line after title
|
||||
|
||||
# Add original commits section
|
||||
if commit_messages:
|
||||
parts.append("Original commits:")
|
||||
parts.append("") # Empty line before list
|
||||
parts.extend(f"* {msg}" for msg in commit_messages)
|
||||
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
def prompt_for_title() -> str:
|
||||
"""Prompt user for a commit title."""
|
||||
return input("Enter commit title (or press Enter to use default): ").strip()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def workspace_manager(original_branch: str):
|
||||
"""Context manager to handle workspace state and cleanup."""
|
||||
stash_created = False
|
||||
stash_restored = False
|
||||
temp_branch: str | None = None
|
||||
|
||||
def cleanup_handler(signum=None, frame=None):
|
||||
"""Clean up workspace state."""
|
||||
nonlocal temp_branch, stash_created, stash_restored
|
||||
try:
|
||||
if signum and stash_restored:
|
||||
# If we're handling Ctrl+C but stash was already restored,
|
||||
# just clean up branches and exit
|
||||
current = get_current_branch()
|
||||
if current and current != original_branch:
|
||||
run_command(f"git checkout {original_branch}")
|
||||
if temp_branch:
|
||||
run_command(f"git branch -D {temp_branch}")
|
||||
print("\nOperation interrupted, but changes were already restored.")
|
||||
sys.exit(3)
|
||||
|
||||
# First, switch back to original branch
|
||||
current = get_current_branch()
|
||||
if current and current != original_branch:
|
||||
run_command(f"git checkout {original_branch}")
|
||||
|
||||
# Then clean up temp branch
|
||||
if temp_branch:
|
||||
run_command(f"git branch -D {temp_branch}")
|
||||
|
||||
# Finally, restore stash if needed - AFTER switching branches
|
||||
if stash_created and not stash_restored:
|
||||
print("Restoring your uncommitted changes...")
|
||||
code, stash_list, _ = run_command("git stash list")
|
||||
if code == 0 and "Automatic stash by squash script" in stash_list:
|
||||
run_command("git stash pop")
|
||||
stash_restored = True
|
||||
stash_created = False
|
||||
|
||||
if signum:
|
||||
print("\nOperation interrupted. Cleaned up and restored original state.")
|
||||
sys.exit(4)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during cleanup: {e}")
|
||||
if signum:
|
||||
sys.exit(5)
|
||||
|
||||
try:
|
||||
# Set up signal handlers
|
||||
signal.signal(signal.SIGINT, cleanup_handler)
|
||||
signal.signal(signal.SIGTERM, cleanup_handler)
|
||||
|
||||
# Check for changes (including untracked files)
|
||||
code, output, _ = run_command("git status --porcelain")
|
||||
if output:
|
||||
print("Stashing uncommitted changes...")
|
||||
run_command("git stash push -u -m 'Automatic stash by squash script'")
|
||||
stash_created = True
|
||||
|
||||
yield lambda x: setattr(x, 'temp_branch', temp_branch)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError occurred: {str(e)}")
|
||||
cleanup_handler()
|
||||
raise
|
||||
finally:
|
||||
cleanup_handler()
|
||||
|
||||
|
||||
def create_commit_with_message(message: str) -> bool:
|
||||
"""Create a commit with the given message using a temporary file."""
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
||||
f.write(message)
|
||||
temp_path = f.name
|
||||
|
||||
# Use the temporary file for the commit message
|
||||
code, _, error = run_command(f"git commit -F {temp_path}")
|
||||
os.unlink(temp_path) # Clean up the temp file
|
||||
|
||||
if code != 0:
|
||||
print(f"Error creating commit: {error}")
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error handling commit message: {e}")
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
return False
|
||||
|
||||
|
||||
def squash_and_merge(source_branch: str, target_branch: str, manual_title: str | None, backup: bool = False, push: bool = False) -> bool:
|
||||
"""
|
||||
Runs a git command and returns the trimmed stdout output.
|
||||
Exits the script if the command fails.
|
||||
Squash the source branch and merge into target branch.
|
||||
"""
|
||||
print(f"Running: {' '.join(command)}")
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
if check and result.returncode != 0:
|
||||
print(result.stdout.strip())
|
||||
print(result.stderr.strip())
|
||||
sys.exit(result.returncode)
|
||||
return result.stdout.strip()
|
||||
# Get original branch right away
|
||||
original_branch = get_current_branch()
|
||||
if not original_branch:
|
||||
return False
|
||||
|
||||
class State:
|
||||
temp_branch: str | None = None
|
||||
|
||||
state = State()
|
||||
|
||||
with workspace_manager(original_branch) as set_temp_branch:
|
||||
# Validate source branch exists
|
||||
code, _, error = run_command(f"git rev-parse --verify {source_branch}")
|
||||
if code != 0:
|
||||
print(f"Error: Source branch {source_branch} not found")
|
||||
return False
|
||||
|
||||
if source_branch == target_branch:
|
||||
print(f"Error: Source and target branches cannot be the same ({source_branch})")
|
||||
return False
|
||||
|
||||
# Ensure target branch exists
|
||||
code, _, error = run_command(f"git rev-parse --verify {target_branch}")
|
||||
if code != 0:
|
||||
print(f"Error: Target branch {target_branch} not found")
|
||||
return False
|
||||
|
||||
# Find merge base
|
||||
code, merge_base, error = run_command(f"git merge-base {target_branch} {source_branch}")
|
||||
if code != 0:
|
||||
print(f"Error finding merge base: {error}")
|
||||
return False
|
||||
|
||||
# Create backup unless explicitly skipped
|
||||
if backup and not backup_branch(source_branch):
|
||||
return False
|
||||
|
||||
# Get commit messages
|
||||
commit_messages = get_commit_messages(source_branch, target_branch)
|
||||
if commit_messages is None:
|
||||
return False
|
||||
|
||||
# Get title (priority: manual title > PR title > prompt user)
|
||||
title = manual_title
|
||||
if not title:
|
||||
title = get_pr_info(source_branch)
|
||||
if not title:
|
||||
title = prompt_for_title()
|
||||
|
||||
try:
|
||||
# Create and switch to temporary branch
|
||||
temp_branch = f"temp-squash-{source_branch}"
|
||||
state.temp_branch = temp_branch
|
||||
set_temp_branch(state)
|
||||
|
||||
print(f"\nCreating temporary branch {temp_branch}...")
|
||||
code, _, error = run_command(f"git checkout -b {temp_branch} {source_branch}")
|
||||
if code != 0:
|
||||
print(f"Error creating temp branch: {error}")
|
||||
return False
|
||||
|
||||
print("Preparing squash by resetting temporary branch to merge base...")
|
||||
code, _, error = run_command(f"git reset --soft {merge_base}")
|
||||
if code != 0:
|
||||
print(f"Error resetting for squash: {error}")
|
||||
return False
|
||||
|
||||
# Create commit with message
|
||||
print("Creating squash commit...")
|
||||
squash_message = create_squash_message(title, commit_messages, source_branch)
|
||||
if not create_commit_with_message(squash_message):
|
||||
return False
|
||||
|
||||
# Switch to target and try merge
|
||||
print(f"\nSwitching to target branch {target_branch}...")
|
||||
code, _, error = run_command(f"git checkout {target_branch}")
|
||||
if code != 0:
|
||||
print(f"Error checking out target branch: {error}")
|
||||
return False
|
||||
|
||||
print(f"Attempting to merge changes from {temp_branch}...")
|
||||
code, _, error = run_command(f"git rebase {temp_branch}")
|
||||
|
||||
if code != 0:
|
||||
print(f"\nMerge failed with error: {error}")
|
||||
print("\nThe squash was successful, and your changes are preserved in the temporary branch.")
|
||||
print("To complete the merge manually, follow these steps:")
|
||||
print(f"\n1. Your squashed changes are in branch: '{temp_branch}'")
|
||||
print(f"2. The target branch is: '{target_branch}'")
|
||||
print("\nTo resolve the conflicts:")
|
||||
print(f" git checkout {target_branch}")
|
||||
print(f" git merge {temp_branch}")
|
||||
print(" # resolve conflicts in your editor")
|
||||
print(" git add <resolved-files>")
|
||||
print(" git commit")
|
||||
print(f" git push origin {target_branch} # when ready to push")
|
||||
print("\nTo clean up after successful merge:")
|
||||
print(f" git branch -D {temp_branch}")
|
||||
|
||||
# Make sure to abort the merge
|
||||
print("\nAborting current merge attempt...")
|
||||
run_command("git merge --abort")
|
||||
|
||||
# Return to original branch, but keep temp branch
|
||||
print(f"Returning to {original_branch}...")
|
||||
run_command(f"git checkout {original_branch}")
|
||||
return False
|
||||
|
||||
# Clean up temp branch on success
|
||||
run_command(f"git branch -D {temp_branch}")
|
||||
|
||||
# Push if requested
|
||||
if push:
|
||||
code, _, error = run_command(f"git push origin {target_branch}")
|
||||
if code != 0:
|
||||
print(f"Error pushing to {target_branch}: {error}")
|
||||
return False
|
||||
print(f"Successfully pushed to {target_branch}")
|
||||
else:
|
||||
print(f"Changes squashed and merged into {target_branch} locally")
|
||||
print(f"To push the changes: git push origin {target_branch}")
|
||||
|
||||
# Return to original branch
|
||||
code, _, error = run_command(f"git checkout {original_branch}")
|
||||
if code != 0:
|
||||
print(f"Warning: Failed to return to original branch: {error}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during squash process: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Merge multiple branches with squash merges.")
|
||||
parser.add_argument("--base", required=True, help="The base branch name from which the target branch will be created.")
|
||||
parser.add_argument("--target", required=True, help="The target branch name to merge into.")
|
||||
parser.add_argument("--title", required=False, help="Title for the commit")
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Squash branch and merge into target branch'
|
||||
)
|
||||
parser.add_argument('--target', '-t', required=True,
|
||||
help='Target branch to merge changes into')
|
||||
parser.add_argument('--source', '-s',
|
||||
help='Source branch to squash (default: current branch)')
|
||||
parser.add_argument('--title', '-m',
|
||||
help='Optional manual title (overrides PR title)')
|
||||
parser.add_argument('--backup', action='store_true',
|
||||
help='Creates a backup branch for the source branch')
|
||||
parser.add_argument('--push', action='store_true',
|
||||
help='Push changes to remote after squashing')
|
||||
|
||||
parser.add_argument("branches", nargs="+", help="List of branch names to merge into the target branch.")
|
||||
args = parser.parse_args()
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
# Checkout the base branch to ensure a common starting point.
|
||||
run_git_command(["git", "checkout", args.base])
|
||||
# Determine source branch early
|
||||
source_branch = args.source
|
||||
if not source_branch:
|
||||
source_branch = get_current_branch()
|
||||
if not source_branch:
|
||||
sys.exit(1)
|
||||
|
||||
# Check if the target branch exists. If not, create it from the base branch.
|
||||
branch_list = run_git_command(["git", "branch"], check=False)
|
||||
branch_names = [line.strip("* ").strip() for line in branch_list.splitlines()]
|
||||
if args.target in branch_names:
|
||||
run_git_command(["git", "checkout", args.target])
|
||||
else:
|
||||
run_git_command(["git", "checkout", "-b", args.target])
|
||||
|
||||
# Iterate over each branch, merging it with a squash merge.
|
||||
for branch in args.branches:
|
||||
print(f"Merging branch '{branch}' with a squash merge.")
|
||||
# Merge the branch without creating a merge commit.
|
||||
run_git_command(["git", "merge", "--squash", branch])
|
||||
# Commit the squashed changes with an appropriate message.
|
||||
commit_message = args.title or f"Squashed merge of branch '{branch}'"
|
||||
run_git_command(["git", "commit", "-m", commit_message])
|
||||
|
||||
print(f"All branches have been merged with squashed commits into '{args.target}'.")
|
||||
if not squash_and_merge(source_branch, args.target, args.title, args.backup, args.push):
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -80,6 +80,7 @@ def add_pr_comment(pr_number, comment):
|
||||
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')
|
||||
@@ -142,30 +143,26 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path):
|
||||
subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True)
|
||||
|
||||
# Run squash script
|
||||
result = subprocess.run([
|
||||
subprocess.run([
|
||||
squash_script_path,
|
||||
'--target', target_branch,
|
||||
'--base', source_branch,
|
||||
'--title', f"{title} (PR-{pr_number})",
|
||||
branch,
|
||||
], capture_output=True, text=True)
|
||||
'--source', branch,
|
||||
'--title', f"{title} (#{pr_number})",
|
||||
], check=True)
|
||||
|
||||
print(result.stdout)
|
||||
if result.returncode == 0:
|
||||
print(f"Successfully processed PR #{pr_number}")
|
||||
success_count += 1
|
||||
continue
|
||||
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 {result.returncode}")
|
||||
output = result.stdout
|
||||
print(f"Error output: {output}")
|
||||
add_pr_comment(pr_number, f"⚠️ Error during automated `{target_branch}` squash:\n```\n{output}\n```")
|
||||
subprocess.run(['git', 'reset', '--hard'], check=True)
|
||||
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)}")
|
||||
subprocess.run(['git', 'reset', '--hard'], check=True)
|
||||
continue
|
||||
|
||||
return success_count
|
||||
|
||||
@@ -108,7 +108,7 @@ 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)
|
||||
sunnypilot_interfaces.setup_interfaces(self.CI.CP, self.CI.CP_SP, self.params)
|
||||
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
|
||||
@@ -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)
|
||||
sunnypilot_interfaces.init_interfaces(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)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class TestCarInterfaces:
|
||||
experimental_long=args['experimental_long'], docs=False)
|
||||
car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'],
|
||||
experimental_long=args['experimental_long'], docs=False)
|
||||
sunnypilot_interfaces.setup_interfaces(car_params, car_params_sp)
|
||||
sunnypilot_interfaces.setup_car_interface_sp(car_params, car_params_sp)
|
||||
car_params = car_params.as_reader()
|
||||
car_interface = CarInterface(car_params, car_params_sp)
|
||||
assert car_params
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from cereal import log
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController
|
||||
|
||||
LaneChangeState = log.LaneChangeState
|
||||
LaneChangeDirection = log.LaneChangeDirection
|
||||
@@ -40,10 +39,8 @@ class DesireHelper:
|
||||
self.keep_pulse_timer = 0.0
|
||||
self.prev_one_blinker = False
|
||||
self.desire = log.Desire.none
|
||||
self.alc = AutoLaneChangeController(self)
|
||||
|
||||
def update(self, carstate, lateral_active, lane_change_prob):
|
||||
self.alc.update_params()
|
||||
v_ego = carstate.vEgo
|
||||
one_blinker = carstate.leftBlinker != carstate.rightBlinker
|
||||
below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN
|
||||
@@ -70,12 +67,10 @@ class DesireHelper:
|
||||
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
|
||||
|
||||
if not one_blinker or below_lane_change_speed:
|
||||
self.lane_change_state = LaneChangeState.off
|
||||
self.lane_change_direction = LaneChangeDirection.none
|
||||
elif (torque_applied or self.alc.auto_lane_change_allowed) and not blindspot_detected:
|
||||
elif torque_applied and not blindspot_detected:
|
||||
self.lane_change_state = LaneChangeState.laneChangeStarting
|
||||
|
||||
# LaneChangeState.laneChangeStarting
|
||||
@@ -117,5 +112,3 @@ class DesireHelper:
|
||||
self.keep_pulse_timer = 0.0
|
||||
elif self.desire in (log.Desire.keepLeft, log.Desire.keepRight):
|
||||
self.desire = log.Desire.none
|
||||
|
||||
self.alc.update_state()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
from cereal import custom
|
||||
from cereal import log
|
||||
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
@@ -58,23 +58,23 @@ STOP_DISTANCE = 6.0
|
||||
CRUISE_MIN_ACCEL = -1.2
|
||||
CRUISE_MAX_ACCEL = 1.6
|
||||
|
||||
def get_jerk_factor(personality=custom.LongitudinalPersonalitySP.standard):
|
||||
if personality==custom.LongitudinalPersonalitySP.relaxed:
|
||||
def get_jerk_factor(personality=log.LongitudinalPersonality.standard):
|
||||
if personality==log.LongitudinalPersonality.relaxed:
|
||||
return 1.0
|
||||
elif personality==custom.LongitudinalPersonalitySP.standard:
|
||||
elif personality==log.LongitudinalPersonality.standard:
|
||||
return 1.0
|
||||
elif personality==custom.LongitudinalPersonalitySP.aggressive:
|
||||
elif personality==log.LongitudinalPersonality.aggressive:
|
||||
return 0.5
|
||||
else:
|
||||
raise NotImplementedError("Longitudinal personality not supported")
|
||||
|
||||
|
||||
def get_T_FOLLOW(personality=custom.LongitudinalPersonalitySP.standard):
|
||||
if personality==custom.LongitudinalPersonalitySP.relaxed:
|
||||
def get_T_FOLLOW(personality=log.LongitudinalPersonality.standard):
|
||||
if personality==log.LongitudinalPersonality.relaxed:
|
||||
return 1.75
|
||||
elif personality==custom.LongitudinalPersonalitySP.standard:
|
||||
elif personality==log.LongitudinalPersonality.standard:
|
||||
return 1.45
|
||||
elif personality==custom.LongitudinalPersonalitySP.aggressive:
|
||||
elif personality==log.LongitudinalPersonality.aggressive:
|
||||
return 1.25
|
||||
else:
|
||||
raise NotImplementedError("Longitudinal personality not supported")
|
||||
@@ -274,7 +274,7 @@ class LongitudinalMpc:
|
||||
for i in range(N):
|
||||
self.solver.cost_set(i, 'Zl', Zl)
|
||||
|
||||
def set_weights(self, prev_accel_constraint=True, personality=custom.LongitudinalPersonalitySP.standard):
|
||||
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
|
||||
jerk_factor = get_jerk_factor(personality)
|
||||
if self.mode == 'acc':
|
||||
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
|
||||
@@ -327,7 +327,7 @@ class LongitudinalMpc:
|
||||
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
||||
return lead_xv
|
||||
|
||||
def update(self, radarstate, v_cruise, x, v, a, j, personality=custom.LongitudinalPersonalitySP.standard):
|
||||
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard):
|
||||
t_follow = get_T_FOLLOW(personality)
|
||||
v_ego = self.x0[1]
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
@@ -162,11 +163,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
if force_slow_decel:
|
||||
v_cruise = 0.0
|
||||
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveStateSP'].personality)
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveStateSP'].personality)
|
||||
|
||||
|
||||
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality)
|
||||
|
||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||
@@ -194,7 +193,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
def publish(self, sm, pm):
|
||||
plan_send = messaging.new_message('longitudinalPlan')
|
||||
|
||||
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'selfdriveStateSP'])
|
||||
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState'])
|
||||
|
||||
longitudinalPlan = plan_send.longitudinalPlan
|
||||
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
||||
|
||||
@@ -23,7 +23,7 @@ class TestLatControl:
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
sunnypilot_interfaces.setup_interfaces(CP, CP_SP)
|
||||
sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP)
|
||||
CP_SP = convert_to_capnp(CP_SP)
|
||||
VM = VehicleModel(CP)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ def main():
|
||||
ldw = LaneDepartureWarning()
|
||||
longitudinal_planner = LongitudinalPlanner(CP)
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState', 'selfdriveStateSP'],
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'],
|
||||
poll='modelV2')
|
||||
|
||||
while True:
|
||||
|
||||
@@ -32,7 +32,7 @@ REPLAY = "REPLAY" in os.environ
|
||||
SIMULATION = "SIMULATION" in os.environ
|
||||
TESTING_CLOSET = "TESTING_CLOSET" in os.environ
|
||||
IGNORE_PROCESSES = {"loggerd", "encoderd", "statsd"}
|
||||
LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in custom.LongitudinalPersonalitySP.schema.enumerants.items()}
|
||||
LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in log.LongitudinalPersonality.schema.enumerants.items()}
|
||||
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
State = log.SelfdriveState.OpenpilotState
|
||||
@@ -475,6 +475,7 @@ class SelfdriveD(CruiseHelper):
|
||||
ss.state = self.state_machine.state
|
||||
ss.engageable = not self.events.contains(ET.NO_ENTRY)
|
||||
ss.experimentalMode = self.experimental_mode
|
||||
ss.personality = self.personality
|
||||
|
||||
ss.alertText1 = self.AM.current_alert.alert_text_1
|
||||
ss.alertText2 = self.AM.current_alert.alert_text_2
|
||||
@@ -503,15 +504,14 @@ class SelfdriveD(CruiseHelper):
|
||||
mads.enabled = self.mads.enabled
|
||||
mads.active = self.mads.active
|
||||
mads.available = self.mads.enabled_toggle
|
||||
ss_sp.personality = self.personality
|
||||
|
||||
self.pm.send('selfdriveStateSP', ss_sp_msg)
|
||||
|
||||
# onroadEventsSP - logged every second or on change
|
||||
if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events_sp.names != self.events_sp_prev):
|
||||
ce_send_sp = messaging.new_message('onroadEventsSP')
|
||||
ce_send_sp = messaging.new_message('onroadEventsSP', len(self.events_sp))
|
||||
ce_send_sp.valid = True
|
||||
ce_send_sp.onroadEventsSP.events = self.events_sp.to_msg()
|
||||
ce_send_sp.onroadEventsSP = self.events_sp.to_msg()
|
||||
self.pm.send('onroadEventsSP', ce_send_sp)
|
||||
self.events_sp_prev = self.events_sp.names.copy()
|
||||
|
||||
@@ -532,7 +532,7 @@ class SelfdriveD(CruiseHelper):
|
||||
try:
|
||||
return int(self.params.get('LongitudinalPersonality'))
|
||||
except (ValueError, TypeError):
|
||||
return custom.LongitudinalPersonalitySP.standard
|
||||
return log.LongitudinalPersonality.standard
|
||||
|
||||
def params_thread(self, evt):
|
||||
while not evt.is_set():
|
||||
|
||||
@@ -25,7 +25,6 @@ class Plant:
|
||||
Plant.car_state = messaging.pub_sock('carState')
|
||||
Plant.plan = messaging.sub_sock('longitudinalPlan')
|
||||
Plant.messaging_initialized = True
|
||||
Plant.selfdrive_state_sp = messaging.pub_sock('selfdriveStateSP')
|
||||
|
||||
self.v_lead_prev = 0.0
|
||||
|
||||
@@ -64,7 +63,6 @@ class Plant:
|
||||
radar = messaging.new_message('radarState')
|
||||
control = messaging.new_message('controlsState')
|
||||
ss = messaging.new_message('selfdriveState')
|
||||
ss_sp = messaging.new_message('selfdriveStateSP')
|
||||
car_state = messaging.new_message('carState')
|
||||
lp = messaging.new_message('liveParameters')
|
||||
car_control = messaging.new_message('carControl')
|
||||
@@ -120,7 +118,7 @@ class Plant:
|
||||
|
||||
control.controlsState.longControlState = LongCtrlState.pid if self.enabled else LongCtrlState.off
|
||||
ss.selfdriveState.experimentalMode = self.e2e
|
||||
ss_sp.selfdriveStateSP.personality = self.personality
|
||||
ss.selfdriveState.personality = self.personality
|
||||
control.controlsState.forceDecel = self.force_decel
|
||||
car_state.carState.vEgo = float(self.speed)
|
||||
car_state.carState.standstill = bool(self.speed < 0.01)
|
||||
@@ -133,7 +131,6 @@ class Plant:
|
||||
'carControl': car_control.carControl,
|
||||
'controlsState': control.controlsState,
|
||||
'selfdriveState': ss.selfdriveState,
|
||||
'selfdriveStateSP': ss_sp.selfdriveStateSP,
|
||||
'liveParameters': lp.liveParameters,
|
||||
'modelV2': model.modelV2}
|
||||
self.planner.update(sm)
|
||||
|
||||
@@ -70,7 +70,9 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) {
|
||||
checkForUpdates();
|
||||
}
|
||||
});
|
||||
addItem(targetBranchBtn);
|
||||
if (!params.getBool("IsTestedBranch")) {
|
||||
addItem(targetBranchBtn);
|
||||
}
|
||||
|
||||
// uninstall button
|
||||
auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL"));
|
||||
|
||||
@@ -13,8 +13,6 @@ from openpilot.common.swaglog import cloudlog
|
||||
|
||||
from openpilot.system import micd
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.quiet_mode import QuietMode
|
||||
|
||||
SAMPLE_RATE = 48000
|
||||
SAMPLE_BUFFER = 4096 # (approx 100ms)
|
||||
MAX_VOLUME = 1.0
|
||||
@@ -52,10 +50,8 @@ def check_selfdrive_timeout_alert(sm):
|
||||
return False
|
||||
|
||||
|
||||
class Soundd(QuietMode):
|
||||
class Soundd:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.load_sounds()
|
||||
|
||||
self.current_alert = AudibleAlert.none
|
||||
@@ -85,7 +81,7 @@ class Soundd(QuietMode):
|
||||
|
||||
ret = np.zeros(frames, dtype=np.float32)
|
||||
|
||||
if self.should_play_sound(self.current_alert):
|
||||
if self.current_alert != AudibleAlert.none:
|
||||
num_loops = sound_list[self.current_alert][1]
|
||||
sound_data = self.loaded_sounds[self.current_alert]
|
||||
written_frames = 0
|
||||
@@ -148,8 +144,6 @@ class Soundd(QuietMode):
|
||||
while True:
|
||||
sm.update(0)
|
||||
|
||||
self.load_param()
|
||||
|
||||
if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
|
||||
self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb)
|
||||
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
|
||||
|
||||
@@ -35,7 +35,6 @@ qt_src = [
|
||||
]
|
||||
|
||||
lateral_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/mads_settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc",
|
||||
]
|
||||
|
||||
@@ -16,26 +16,23 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
||||
device_grid_layout->setHorizontalSpacing(5);
|
||||
device_grid_layout->setVerticalSpacing(25);
|
||||
|
||||
std::vector<std::tuple<QString, QString, QString>> device_btns = {
|
||||
{"quietModeBtn", tr("Quiet Mode"), "QuietMode"},
|
||||
{"dcamBtn", tr("Driver Camera Preview"), ""},
|
||||
{"retrainingBtn", tr("Training Guide"), ""},
|
||||
{"regulatoryBtn", tr("Regulatory"), ""},
|
||||
{"translateBtn", tr("Language"), ""},
|
||||
{"resetParams", tr("Reset Settings"), ""},
|
||||
std::vector<std::pair<QString, QString>> device_btns = {
|
||||
{"dcamBtn", tr("Driver Camera Preview")},
|
||||
{"retrainingBtn", tr("Training Guide")},
|
||||
{"regulatoryBtn", tr("Regulatory")},
|
||||
{"translateBtn", tr("Language")},
|
||||
{"resetParams", tr("Reset Settings")},
|
||||
};
|
||||
|
||||
int row = 0, col = 0;
|
||||
for (const auto &[id, text, param] : device_btns) {
|
||||
if (id == "regulatoryBtn" && !Hardware::TICI()) {
|
||||
for (int i = 0; i < device_btns.size(); i++) {
|
||||
if (device_btns[i].first == "regulatoryBtn" && !Hardware::TICI()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *btn = new PushButtonSP(text, 720, this, param);
|
||||
btn->setObjectName(id);
|
||||
|
||||
auto *btn = new PushButtonSP(device_btns[i].second, 720, this);
|
||||
device_grid_layout->addWidget(btn, row, col);
|
||||
buttons[id] = btn;
|
||||
buttons[device_btns[i].first] = btn;
|
||||
|
||||
col++;
|
||||
if (col > 1) {
|
||||
@@ -46,8 +43,6 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
||||
|
||||
connect(buttons["dcamBtn"], &PushButtonSP::clicked, [=]() { emit showDriverView(); });
|
||||
|
||||
connect(buttons["quietModeBtn"], &PushButtonSP::clicked, buttons["quietModeBtn"], &PushButtonSP::updateButton);
|
||||
|
||||
connect(buttons["retrainingBtn"], &PushButtonSP::clicked, [=]() {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) {
|
||||
emit reviewTrainingGuide();
|
||||
@@ -72,7 +67,14 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
||||
}
|
||||
});
|
||||
|
||||
connect(buttons["resetParams"], &PushButtonSP::clicked, this, &DevicePanelSP::resetSettings);
|
||||
connect(buttons["resetParams"], &PushButtonSP::clicked, [=]() {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset all sunnypilot settings to default ? This cannot be undone."), tr("Reset"), this)) {
|
||||
int rm = std::system("sudo rm -rf /data/params/d/*");
|
||||
if (rm == 0) {
|
||||
Hardware::reboot();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addItem(device_grid_layout);
|
||||
|
||||
@@ -139,19 +141,6 @@ void DevicePanelSP::setOffroadMode() {
|
||||
updateState();
|
||||
}
|
||||
|
||||
void DevicePanelSP::resetSettings() {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back."), tr("Reset"), this)) {
|
||||
if (ConfirmationDialog::confirm(tr("The reset cannot be undone. You have been warned."), tr("Confirm"), this)) {
|
||||
const std::vector<std::string> keys = params.allKeys();
|
||||
for (const auto& key : keys) {
|
||||
params.remove(key);
|
||||
}
|
||||
|
||||
Hardware::reboot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DevicePanelSP::showEvent(QShowEvent *event) {
|
||||
updateState();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ public:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void setOffroadMode();
|
||||
void updateState();
|
||||
void resetSettings();
|
||||
|
||||
private:
|
||||
std::map<QString, PushButtonSP*> buttons;
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Created by kumar on March 10, 2025
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
LaneChangeSettings::LaneChangeSettings(QWidget* parent) : QWidget(parent) {
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(50, 20, 50, 20);
|
||||
main_layout->setSpacing(20);
|
||||
|
||||
// Back button
|
||||
PanelBackButton* back = new PanelBackButton(tr("Back"));
|
||||
connect(back, &QPushButton::clicked, [=]() { emit backPress(); });
|
||||
main_layout->addWidget(back, 0, Qt::AlignLeft);
|
||||
|
||||
ListWidgetSP *list = new ListWidgetSP(this, false);
|
||||
// param, title, desc, icon
|
||||
std::vector<std::tuple<QString, QString, QString, QString>> toggle_defs{
|
||||
{
|
||||
"AutoLaneChangeBsmDelay",
|
||||
tr("Auto Lane Change: Delay with Blind Spot"),
|
||||
tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
},
|
||||
};
|
||||
|
||||
// Controls: Auto Lane Change Timer
|
||||
autoLaneChangeTimer = new AutoLaneChangeTimer();
|
||||
autoLaneChangeTimer->setUpdateOtherToggles(true);
|
||||
autoLaneChangeTimer->showDescription();
|
||||
connect(autoLaneChangeTimer, &OptionControlSP::updateLabels, autoLaneChangeTimer, &AutoLaneChangeTimer::refresh);
|
||||
connect(autoLaneChangeTimer, &AutoLaneChangeTimer::updateOtherToggles, this, &LaneChangeSettings::updateToggles);
|
||||
list->addItem(autoLaneChangeTimer);
|
||||
|
||||
for (auto &[param, title, desc, icon] : toggle_defs) {
|
||||
auto toggle = new ParamControlSP(param, title, desc, icon, this);
|
||||
list->addItem(toggle);
|
||||
toggles[param.toStdString()] = toggle;
|
||||
}
|
||||
|
||||
main_layout->addWidget(new ScrollViewSP(list, this));
|
||||
}
|
||||
|
||||
void LaneChangeSettings::showEvent(QShowEvent *event) {
|
||||
updateToggles();
|
||||
}
|
||||
|
||||
void LaneChangeSettings::updateToggles() {
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto auto_lane_change_bsm_delay_toggle = toggles["AutoLaneChangeBsmDelay"];
|
||||
auto autoLaneChangeTimer_param = std::atoi(params.get("AutoLaneChangeTimer").c_str());
|
||||
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
|
||||
if (!CP.getEnableBsm()) {
|
||||
params.remove("AutoLaneChangeBsmDelay");
|
||||
}
|
||||
auto_lane_change_bsm_delay_toggle->setEnabled(CP.getEnableBsm() && (autoLaneChangeTimer_param > 0));
|
||||
auto_lane_change_bsm_delay_toggle->refresh();
|
||||
} else {
|
||||
auto_lane_change_bsm_delay_toggle->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto Lane Change Timer (ALCT)
|
||||
AutoLaneChangeTimer::AutoLaneChangeTimer() : OptionControlSP(
|
||||
"AutoLaneChangeTimer",
|
||||
tr("Auto Lane Change by Blinker"),
|
||||
tr("Set a timer to delay the auto lane change operation when the blinker is used. "
|
||||
"No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge.\n"
|
||||
"Please use caution when using this feature. Only use the blinker when traffic and road conditions permit."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
{-1, 5}) {
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void AutoLaneChangeTimer::refresh() {
|
||||
QString option = QString::fromStdString(params.get("AutoLaneChangeTimer"));
|
||||
const QString second = tr("s");
|
||||
|
||||
static const QMap<QString, QString> options = {
|
||||
{"-1", tr("Off")},
|
||||
{"0", tr("Nudge")},
|
||||
{"1", tr("Nudgeless")},
|
||||
{"2", "0.5 " + second},
|
||||
{"3", "1 " + second},
|
||||
{"4", "2 " + second},
|
||||
{"5", "3 " + second},
|
||||
};
|
||||
|
||||
setLabel(options.value(option, tr("Nudge")));
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* 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 <map>
|
||||
#include <string>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
class AutoLaneChangeTimer : public OptionControlSP {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AutoLaneChangeTimer();
|
||||
|
||||
void refresh();
|
||||
|
||||
signals:
|
||||
void toggleUpdated();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
};
|
||||
|
||||
|
||||
class LaneChangeSettings : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LaneChangeSettings(QWidget* parent = nullptr);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
signals:
|
||||
void backPress();
|
||||
|
||||
public slots:
|
||||
void updateToggles();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
std::map<std::string, ParamControlSP*> toggles;
|
||||
|
||||
AutoLaneChangeTimer *autoLaneChangeTimer;
|
||||
};
|
||||
@@ -13,6 +13,10 @@ NeuralNetworkLateralControl::NeuralNetworkLateralControl() :
|
||||
updateToggle();
|
||||
}
|
||||
|
||||
void NeuralNetworkLateralControl::showEvent(QShowEvent *event) {
|
||||
updateToggle();
|
||||
}
|
||||
|
||||
void NeuralNetworkLateralControl::updateToggle() {
|
||||
QString statusInitText = "<font color='yellow'>" + STATUS_CHECK_COMPATIBILITY + "</font>";
|
||||
QString notLoadedText = "<font color='yellow'>" + STATUS_NOT_LOADED + "</font>";
|
||||
|
||||
@@ -18,6 +18,7 @@ class NeuralNetworkLateralControl : public ParamControl {
|
||||
|
||||
public:
|
||||
NeuralNetworkLateralControl();
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
public slots:
|
||||
void updateToggle();
|
||||
@@ -25,6 +26,8 @@ public slots:
|
||||
private:
|
||||
Params params;
|
||||
|
||||
void refresh();
|
||||
|
||||
// Status messages
|
||||
const QString STATUS_NOT_AVAILABLE = tr("NNLC is currently not available on this platform.");
|
||||
const QString STATUS_CHECK_COMPATIBILITY = tr("Start the car to check car compatibility");
|
||||
|
||||
@@ -42,29 +42,6 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
});
|
||||
list->addItem(madsSettingsButton);
|
||||
|
||||
list->addItem(vertical_space());
|
||||
list->addItem(horizontal_line());
|
||||
list->addItem(vertical_space());
|
||||
|
||||
// Lane Change Settings
|
||||
laneChangeSettingsButton = new PushButtonSP(tr("Customize Lane Change"));
|
||||
laneChangeSettingsButton->setObjectName("lane_change_btn");
|
||||
connect(laneChangeSettingsButton, &QPushButton::clicked, [=]() {
|
||||
sunnypilotScroller->setLastScrollPosition();
|
||||
main_layout->setCurrentWidget(laneChangeWidget);
|
||||
});
|
||||
|
||||
laneChangeWidget = new LaneChangeSettings(this);
|
||||
connect(laneChangeWidget, &LaneChangeSettings::backPress, [=]() {
|
||||
sunnypilotScroller->restoreScrollPosition();
|
||||
main_layout->setCurrentWidget(sunnypilotScreen);
|
||||
});
|
||||
list->addItem(laneChangeSettingsButton);
|
||||
|
||||
list->addItem(vertical_space(0));
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// Neural Network Lateral Control
|
||||
nnlcToggle = new NeuralNetworkLateralControl();
|
||||
list->addItem(nnlcToggle);
|
||||
|
||||
@@ -88,7 +65,6 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
|
||||
main_layout->addWidget(sunnypilotScreen);
|
||||
main_layout->addWidget(madsWidget);
|
||||
main_layout->addWidget(laneChangeWidget);
|
||||
|
||||
setStyleSheet(R"(
|
||||
#back_btn {
|
||||
@@ -109,7 +85,6 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
}
|
||||
|
||||
void LateralPanel::showEvent(QShowEvent *event) {
|
||||
nnlcToggle->updateToggle();
|
||||
updateToggles(offroad);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
@@ -39,7 +38,5 @@ private:
|
||||
ParamControl *madsToggle;
|
||||
PushButtonSP *madsSettingsButton;
|
||||
MadsSettings *madsWidget = nullptr;
|
||||
PushButtonSP *laneChangeSettingsButton;
|
||||
LaneChangeSettings *laneChangeWidget = nullptr;
|
||||
NeuralNetworkLateralControl *nnlcToggle = nullptr;
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
|
||||
PanelInfo(" " + tr("sunnylink"), new SunnylinkPanel(this), "../assets/offroad/icon_wifi_strength_full.svg"),
|
||||
PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
|
||||
PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
|
||||
PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
|
||||
PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../assets/images/button_home.png"),
|
||||
PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
|
||||
PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
|
||||
PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"),
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h"
|
||||
|
||||
#include "common/watchdog.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include <QtConcurrent>
|
||||
|
||||
SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
main_layout = new QStackedLayout(this);
|
||||
@@ -77,6 +75,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
description = "<font color='SeaGreen'>"+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ "</font>";
|
||||
} else {
|
||||
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(description);
|
||||
@@ -84,38 +83,7 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
updatePanel();
|
||||
});
|
||||
|
||||
// Backup Settings
|
||||
backupSettings = new PushButtonSP(tr("Backup Settings"), 730, this);
|
||||
backupSettings->setObjectName("backup_btn");
|
||||
connect(backupSettings, &QPushButton::clicked, [=]() {
|
||||
backupSettings->setEnabled(false);
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to backup sunnypilot settings?"), tr("Back Up"), this)) {
|
||||
params.putBool("BackupManager_CreateBackup", true);
|
||||
backup_request_pending = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Restore Settings
|
||||
restoreSettings = new PushButtonSP(tr("Restore Settings"), 730, this);
|
||||
restoreSettings->setObjectName("restore_btn");
|
||||
connect(restoreSettings, &QPushButton::clicked, [=]() {
|
||||
restoreSettings->setEnabled(false);
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to restore the last backed up sunnypilot settings?"), tr("Restore"), this)) {
|
||||
params.put("BackupManager_RestoreVersion", "latest");
|
||||
restore_request_pending = true;
|
||||
}
|
||||
});
|
||||
// Settings Restore and Settings Backup in the same horizontal space
|
||||
auto settings_layout = new QHBoxLayout;
|
||||
settings_layout->setContentsMargins(0, 0, 0, 30);
|
||||
settings_layout->addWidget(backupSettings);
|
||||
settings_layout->addSpacing(10);
|
||||
settings_layout->addWidget(restoreSettings);
|
||||
settings_layout->setAlignment(Qt::AlignLeft);
|
||||
list->addItem(settings_layout);
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel);
|
||||
QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SunnylinkPanel::updatePanel);
|
||||
|
||||
sunnylinkScroller = new ScrollViewSP(list, this);
|
||||
vlayout->addWidget(sunnylinkScroller);
|
||||
@@ -127,76 +95,6 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
}
|
||||
}
|
||||
|
||||
void SunnylinkPanel::updateBackupManagerState() {
|
||||
const SubMaster &sm = *(uiStateSP()->sm);
|
||||
backup_manager = sm["backupManagerSP"].getBackupManagerSP();
|
||||
}
|
||||
|
||||
void SunnylinkPanel::handleBackupProgress() {
|
||||
auto backup_status = backup_manager.getBackupStatus();
|
||||
auto restore_status = backup_manager.getRestoreStatus();
|
||||
auto backup_progress = backup_manager.getBackupProgress();
|
||||
auto restore_progress = backup_manager.getRestoreProgress();
|
||||
|
||||
switch (backup_status) {
|
||||
case cereal::BackupManagerSP::Status::IN_PROGRESS:
|
||||
backup_request_pending = false;
|
||||
backup_request_started = true;
|
||||
backupSettings->setEnabled(false);
|
||||
backupSettings->setText(QString(tr("Backup in progress %1%").arg(backup_progress)));
|
||||
break;
|
||||
case cereal::BackupManagerSP::Status::FAILED:
|
||||
backup_request_pending = false;
|
||||
backup_request_started = false;
|
||||
backupSettings->setEnabled(!is_onroad);
|
||||
backupSettings->setText(tr("Backup Failed"));
|
||||
break;
|
||||
case cereal::BackupManagerSP::Status::COMPLETED:
|
||||
backup_request_pending = false;
|
||||
break;
|
||||
default:
|
||||
if (!backup_request_pending && backup_request_started) {
|
||||
backup_request_started = false;
|
||||
ConfirmationDialog::alert(tr("Settings backup completed."), this);
|
||||
} else {
|
||||
backupSettings->setEnabled(!is_onroad && !backup_request_pending && is_sunnylink_enabled);
|
||||
}
|
||||
backupSettings->setText(tr("Backup Settings"));
|
||||
break;
|
||||
}
|
||||
|
||||
switch (restore_status) {
|
||||
case cereal::BackupManagerSP::Status::IN_PROGRESS:
|
||||
restore_request_pending = false;
|
||||
restore_request_started = true;
|
||||
restoreSettings->setEnabled(false);
|
||||
restoreSettings->setText(QString(tr("Restore in progress %1%").arg(restore_progress)));
|
||||
break;
|
||||
case cereal::BackupManagerSP::Status::FAILED:
|
||||
restore_request_pending = false;
|
||||
restore_request_started = false;
|
||||
restoreSettings->setEnabled(!is_onroad);
|
||||
restoreSettings->setText(tr("Restore Failed"));
|
||||
ConfirmationDialog::alert(tr("Unable to restore the settings, try again later."), this);
|
||||
break;
|
||||
case cereal::BackupManagerSP::Status::COMPLETED:
|
||||
restore_request_pending = false;
|
||||
break;
|
||||
default:
|
||||
if (!restore_request_pending && restore_request_started) {
|
||||
restore_request_started = false;
|
||||
if (ConfirmationDialog::alert(tr("Settings restored. Confirm to restart the interface."), this)) {
|
||||
qApp->exit(18);
|
||||
watchdog_kick(0);
|
||||
}
|
||||
} else {
|
||||
restoreSettings->setEnabled(!is_onroad && !restore_request_pending && is_sunnylink_enabled);
|
||||
}
|
||||
restoreSettings->setText(tr("Restore Settings"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SunnylinkPanel::paramsRefresh(const QString ¶m_name, const QString ¶m_value) {
|
||||
// We do it on paramsRefresh because the toggleEvent happens before the value is updated
|
||||
if (param_name == "SunnylinkEnabled" && param_value == "1") {
|
||||
@@ -230,7 +128,7 @@ void SunnylinkPanel::stopSunnylink() const {
|
||||
void SunnylinkPanel::showEvent(QShowEvent *event) {
|
||||
updatePanel();
|
||||
if (is_sunnylink_enabled) {
|
||||
startSunnylink();
|
||||
startSunnylink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +137,6 @@ void SunnylinkPanel::updatePanel() {
|
||||
return;
|
||||
}
|
||||
|
||||
updateBackupManagerState();
|
||||
handleBackupProgress();
|
||||
const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A"));
|
||||
sunnylinkEnabledBtn->setEnabled(!is_onroad);
|
||||
|
||||
@@ -256,12 +152,13 @@ void SunnylinkPanel::updatePanel() {
|
||||
sunnylinkEnabledBtn->setValue(tr("Device ID") + " " + sunnylinkDongleId);
|
||||
|
||||
sponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled);
|
||||
sponsorBtn->setText(is_sub ? tr("THANKS ♥") : tr("SPONSOR"));
|
||||
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("");
|
||||
|
||||
@@ -19,9 +19,7 @@ class SunnylinkPanel : public QFrame {
|
||||
public:
|
||||
explicit SunnylinkPanel(QWidget *parent = nullptr);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void paramsRefresh(const QString ¶m_name, const QString ¶m_value);
|
||||
void updateBackupManagerState();
|
||||
void handleBackupProgress();
|
||||
void paramsRefresh(const QString¶m_name, const QString¶m_value);
|
||||
|
||||
public slots:
|
||||
void updatePanel();
|
||||
@@ -33,23 +31,17 @@ private:
|
||||
ScrollViewSP *sunnylinkScroller = nullptr;
|
||||
SunnylinkSponsorPopup *status_popup;
|
||||
SunnylinkSponsorPopup *pair_popup;
|
||||
ButtonControlSP *sponsorBtn;
|
||||
ButtonControlSP *pairSponsorBtn;
|
||||
SunnylinkClient *sunnylink_client;
|
||||
cereal::BackupManagerSP::Reader backup_manager;
|
||||
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;
|
||||
bool backup_request_pending = false;
|
||||
bool backup_request_started = false;
|
||||
bool restore_request_pending = false;
|
||||
bool restore_request_started = false;
|
||||
ParamWatcher *param_watcher;
|
||||
QString sunnylinkBtnDescription;
|
||||
PushButtonSP *restoreSettings;
|
||||
PushButtonSP *backupSettings;
|
||||
|
||||
void stopSunnylink() const;
|
||||
void startSunnylink() const;
|
||||
};
|
||||
|
||||
@@ -48,19 +48,8 @@ PlatformSelector::PlatformSelector() : ButtonControl(tr("Vehicle"), "", "") {
|
||||
|
||||
void PlatformSelector::refresh(bool _offroad) {
|
||||
QString name = getPlatformBundle("name").toString();
|
||||
if (!name.isEmpty()) {
|
||||
setValue(name);
|
||||
setText(tr("REMOVE"));
|
||||
} else {
|
||||
setText(tr("SEARCH"));
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
setValue(QString::fromStdString(CP.getCarFingerprint().cStr()));
|
||||
}
|
||||
}
|
||||
setValue(name);
|
||||
setText(name.isEmpty() ? tr("SEARCH") : tr("REMOVE"));
|
||||
setEnabled(true);
|
||||
|
||||
offroad = _offroad;
|
||||
|
||||
@@ -22,13 +22,6 @@ QFrame *horizontal_line(QWidget *parent) {
|
||||
return line;
|
||||
}
|
||||
|
||||
QFrame *vertical_space(int height, QWidget *parent) {
|
||||
QFrame *v_space = new QFrame(parent);
|
||||
v_space->setFrameShape(QFrame::StyledPanel);
|
||||
v_space->setFixedHeight(height);
|
||||
return v_space;
|
||||
}
|
||||
|
||||
// AbstractControlSP
|
||||
|
||||
AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/toggle.h"
|
||||
|
||||
QFrame *horizontal_line(QWidget *parent = nullptr);
|
||||
QFrame *vertical_space(int height = 10, QWidget *parent = nullptr);
|
||||
|
||||
inline void ReplaceWidget(QWidget *old_widget, QWidget *new_widget) {
|
||||
if (old_widget && old_widget->parentWidget() && old_widget->parentWidget()->layout()) {
|
||||
@@ -521,11 +520,7 @@ protected:
|
||||
}
|
||||
|
||||
// Draw the rectangle
|
||||
#ifdef __APPLE__
|
||||
QRect rect(0, !_title.isEmpty() ? (h - 16) : 20, w, h);
|
||||
#else
|
||||
QRect rect(0, !_title.isEmpty() ? (h - 24) : 20, w, h);
|
||||
#endif
|
||||
p.setBrush(QColor(button_enabled ? "#b24a4a4a" : "#121212")); // Background color
|
||||
p.setPen(QPen(Qt::NoPen));
|
||||
p.drawRoundedRect(rect, 20, 20);
|
||||
@@ -555,8 +550,8 @@ class PushButtonSP : public QPushButton {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PushButtonSP(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr, const QString ¶m = "") : QPushButton(text, parent) {
|
||||
buttonStyle = R"(
|
||||
PushButtonSP(const QString &text, const int minimum_button_width = 800, QWidget *parent = nullptr) : QPushButton(text, parent) {
|
||||
const QString buttonStyle = R"(
|
||||
QPushButton {
|
||||
border-radius: 20px;
|
||||
font-size: 50px;
|
||||
@@ -565,61 +560,21 @@ public:
|
||||
padding: 0 25px 0 25px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
QPushButton:enabled {
|
||||
background-color: #393939;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #4A4A4A;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #121212;
|
||||
color: #5C5C5C;
|
||||
}
|
||||
)";
|
||||
|
||||
if (!param.isEmpty()) {
|
||||
key = param.toStdString();
|
||||
refresh();
|
||||
} else {
|
||||
updateStyle(false);
|
||||
}
|
||||
|
||||
setStyleSheet(buttonStyle);
|
||||
setFixedWidth(minimum_button_width);
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
if (!key.empty()) {
|
||||
bool state = params.getBool(key);
|
||||
if (state != is_enabled) {
|
||||
is_enabled = state;
|
||||
}
|
||||
updateStyle(is_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void updateButton() {
|
||||
if (!key.empty()) {
|
||||
params.putBool(key, !is_enabled);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// Override mouse release event to handle style updates smoothly
|
||||
void mouseReleaseEvent(QMouseEvent *event) override {
|
||||
if (!key.empty()) {
|
||||
bool next_state = !params.getBool(key);
|
||||
updateStyle(next_state);
|
||||
}
|
||||
QPushButton::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string key = "";
|
||||
Params params;
|
||||
bool is_enabled;
|
||||
QString buttonStyle;
|
||||
|
||||
QString btn_enabled_off_style = "QPushButton:enabled { background-color: #393939; }";
|
||||
QString btn_enabled_on_style = "QPushButton:enabled { background-color: #1e79e8; }";
|
||||
QString btn_pressed_style = "QPushButton:pressed { background-color: #4A4A4A; }";
|
||||
QString btn_disabled_stype = "QPushButton:disabled { background-color: #121212; color: #5C5C5C; }";
|
||||
|
||||
void updateStyle(bool enabled) {
|
||||
QString enabled_style = enabled ? btn_enabled_on_style : btn_enabled_off_style;
|
||||
|
||||
setStyleSheet(buttonStyle + enabled_style + btn_pressed_style + btn_disabled_stype);
|
||||
}
|
||||
};
|
||||
|
||||
class PanelBackButton : public QPushButton {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"""
|
||||
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 car
|
||||
|
||||
from openpilot.common.params import Params
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
ALERTS_ALWAYS_PLAY = {
|
||||
AudibleAlert.warningSoft,
|
||||
AudibleAlert.warningImmediate,
|
||||
AudibleAlert.promptDistracted,
|
||||
AudibleAlert.promptRepeat,
|
||||
}
|
||||
|
||||
class QuietMode:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.enabled: bool = self.params.get_bool("QuietMode")
|
||||
self._frame = 0
|
||||
|
||||
def load_param(self) -> None:
|
||||
self._frame += 1
|
||||
if self._frame % 50 == 0: # 2.5 seconds
|
||||
self.enabled = self.params.get_bool("QuietMode")
|
||||
|
||||
def should_play_sound(self, current_alert: int) -> bool:
|
||||
"""
|
||||
Check if a sound should be played based on the Quiet Mode setting
|
||||
and the current alert.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return bool(current_alert != AudibleAlert.none)
|
||||
|
||||
return current_alert in ALERTS_ALWAYS_PLAY
|
||||
@@ -18,7 +18,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) {
|
||||
"modelV2", "controlsState", "liveCalibration", "radarState", "deviceState",
|
||||
"pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2",
|
||||
"wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan",
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP"
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP",
|
||||
});
|
||||
|
||||
// update timer
|
||||
|
||||
@@ -151,7 +151,7 @@ def setup_keyboard_uppercase(click, pm: PubMaster, scroll=None):
|
||||
|
||||
def setup_driver_camera(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_device(click, pm)
|
||||
click(1720, 620)
|
||||
click(950, 620)
|
||||
DATA['deviceState'].deviceState.started = False
|
||||
setup_onroad(click, pm)
|
||||
DATA['deviceState'].deviceState.started = True
|
||||
@@ -233,12 +233,6 @@ def setup_settings_steering_mads(click, pm: PubMaster, scroll=None):
|
||||
click(970, 250)
|
||||
time.sleep(UI_DELAY)
|
||||
|
||||
def setup_settings_steering_alc(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_device(click, pm)
|
||||
click(278, 852)
|
||||
click(970, 534)
|
||||
time.sleep(UI_DELAY)
|
||||
|
||||
def setup_settings_trips(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_device(click, pm)
|
||||
click(278, 962)
|
||||
@@ -290,7 +284,6 @@ CASES.update({
|
||||
"settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button,
|
||||
"settings_steering": setup_settings_steering,
|
||||
"settings_steering_mads": setup_settings_steering_mads,
|
||||
"settings_steering_alc": setup_settings_steering_alc,
|
||||
"settings_trips": setup_settings_trips,
|
||||
"settings_vehicle": setup_settings_vehicle,
|
||||
})
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f993debd55727b9ad0c726b8a0d5ec76b146ee3f296b30eba8e53d61b33b24fd
|
||||
size 21750
|
||||
@@ -26,7 +26,7 @@ def log_fingerprint(CP: structs.CarParams) -> None:
|
||||
sentry.capture_fingerprint(CP.carFingerprint, CP.brand)
|
||||
|
||||
|
||||
def _initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None,
|
||||
def initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None,
|
||||
enabled: bool = False) -> None:
|
||||
if params is None:
|
||||
params = Params()
|
||||
@@ -47,7 +47,7 @@ def _initialize_neural_network_lateral_control(CP: structs.CarParams, CP_SP: str
|
||||
CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = not exact_match
|
||||
|
||||
|
||||
def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None) -> None:
|
||||
def setup_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None):
|
||||
if params is None:
|
||||
params = Params()
|
||||
|
||||
@@ -60,14 +60,11 @@ def _initialize_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP,
|
||||
if params.get_bool("HyundaiRadarTracks"):
|
||||
CP.radarUnavailable = False
|
||||
|
||||
|
||||
def setup_interfaces(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None):
|
||||
_initialize_neural_network_lateral_control(CP, CP_SP, params)
|
||||
_initialize_radar_tracks(CP, CP_SP, params)
|
||||
initialize_neural_network_lateral_control(CP, CP_SP, params)
|
||||
|
||||
|
||||
def _enable_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, can_recv: CanRecvCallable,
|
||||
params: Params) -> None:
|
||||
def initialize_car_interface_sp(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params, can_recv: CanRecvCallable,
|
||||
can_send: CanSendCallable):
|
||||
if CP.brand == 'hyundai':
|
||||
if CP_SP.flags & HyundaiFlagsSP.ENABLE_RADAR_TRACKS:
|
||||
can_recv()
|
||||
@@ -82,8 +79,3 @@ def _enable_radar_tracks(CP: structs.CarParams, CP_SP: structs.CarParamsSP, can_
|
||||
if not radar_tracks_persistent:
|
||||
params.put_bool_nonblocking("HyundaiRadarTracks", not radar_unavailable)
|
||||
params.put_bool_nonblocking("HyundaiRadarTracksPersistent", True)
|
||||
|
||||
|
||||
def init_interfaces(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params,
|
||||
can_recv: CanRecvCallable, can_send: CanSendCallable):
|
||||
_enable_radar_tracks(CP, CP_SP, can_recv, params)
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
"""
|
||||
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 log
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
|
||||
class AutoLaneChangeMode:
|
||||
OFF = -1
|
||||
NUDGE = 0 # default
|
||||
NUDGELESS = 1
|
||||
HALF_SECOND = 2
|
||||
ONE_SECOND = 3
|
||||
TWO_SECONDS = 4
|
||||
THREE_SECONDS = 5
|
||||
|
||||
|
||||
AUTO_LANE_CHANGE_TIMER = {
|
||||
AutoLaneChangeMode.OFF: 0.0, # Off
|
||||
AutoLaneChangeMode.NUDGE: 0.0, # Nudge
|
||||
AutoLaneChangeMode.NUDGELESS: 0.05, # Nudgeless
|
||||
AutoLaneChangeMode.HALF_SECOND: 0.5, # 0.5-second delay
|
||||
AutoLaneChangeMode.ONE_SECOND: 1.0, # 1-second delay
|
||||
AutoLaneChangeMode.TWO_SECONDS: 2.0, # 2-second delay
|
||||
AutoLaneChangeMode.THREE_SECONDS: 3.0, # 3-second delay
|
||||
}
|
||||
|
||||
ONE_SECOND_DELAY = -1
|
||||
|
||||
|
||||
class AutoLaneChangeController:
|
||||
def __init__(self, desire_helper):
|
||||
self.DH = desire_helper
|
||||
self.params = Params()
|
||||
|
||||
self.lane_change_wait_timer = 0.0
|
||||
self.param_read_counter = 0
|
||||
self.lane_change_delay = 0.0
|
||||
|
||||
self.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||
self.lane_change_bsm_delay = False
|
||||
|
||||
self.prev_brake_pressed = False
|
||||
self.auto_lane_change_allowed = False
|
||||
self.prev_lane_change = False
|
||||
|
||||
self.read_params()
|
||||
|
||||
def reset(self) -> None:
|
||||
# Auto reset if parent state indicates we should
|
||||
if self.DH.lane_change_state == log.LaneChangeState.off and \
|
||||
self.DH.lane_change_direction == log.LaneChangeDirection.none:
|
||||
self.lane_change_wait_timer = 0.0
|
||||
self.prev_brake_pressed = False
|
||||
self.prev_lane_change = False
|
||||
|
||||
def read_params(self) -> None:
|
||||
self.lane_change_bsm_delay = self.params.get_bool("AutoLaneChangeBsmDelay")
|
||||
try:
|
||||
self.lane_change_set_timer = int(self.params.get("AutoLaneChangeTimer", encoding="utf8"))
|
||||
except (ValueError, TypeError):
|
||||
self.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||
|
||||
def update_params(self) -> None:
|
||||
if self.param_read_counter % 50 == 0:
|
||||
self.read_params()
|
||||
self.param_read_counter += 1
|
||||
|
||||
def update_lane_change_timers(self, blindspot_detected: bool) -> None:
|
||||
self.lane_change_delay = AUTO_LANE_CHANGE_TIMER.get(self.lane_change_set_timer,
|
||||
AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGE])
|
||||
|
||||
self.lane_change_wait_timer += DT_MDL
|
||||
|
||||
if self.lane_change_bsm_delay and blindspot_detected and self.lane_change_delay > 0:
|
||||
if self.lane_change_delay == AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]:
|
||||
self.lane_change_wait_timer = ONE_SECOND_DELAY
|
||||
else:
|
||||
self.lane_change_wait_timer = self.lane_change_delay + ONE_SECOND_DELAY
|
||||
|
||||
def update_allowed(self) -> bool:
|
||||
# Auto lane change allowed if:
|
||||
# 1. A valid delay is set (non-zero)
|
||||
# 2. Brake wasn't previously pressed
|
||||
# 3. We've waited long enough
|
||||
|
||||
if self.lane_change_set_timer in (AutoLaneChangeMode.OFF, AutoLaneChangeMode.NUDGE):
|
||||
return False
|
||||
|
||||
if self.prev_brake_pressed:
|
||||
return False
|
||||
|
||||
if self.prev_lane_change:
|
||||
return False
|
||||
|
||||
return bool(self.lane_change_wait_timer > self.lane_change_delay)
|
||||
|
||||
def update_lane_change(self, blindspot_detected: bool, brake_pressed: bool) -> None:
|
||||
if brake_pressed and not self.prev_brake_pressed:
|
||||
self.prev_brake_pressed = brake_pressed
|
||||
|
||||
self.update_lane_change_timers(blindspot_detected)
|
||||
|
||||
self.auto_lane_change_allowed = self.update_allowed()
|
||||
|
||||
def update_state(self):
|
||||
if self.DH.lane_change_state == log.LaneChangeState.laneChangeStarting:
|
||||
self.prev_lane_change = True
|
||||
|
||||
self.reset()
|
||||
@@ -24,4 +24,4 @@ def get_lag_adjusted_curvature(steer_delay, v_ego, psis, curvatures):
|
||||
current_curvature_desired - max_curvature_rate * DT_MDL,
|
||||
current_curvature_desired + max_curvature_rate * DT_MDL)
|
||||
|
||||
return float(safe_desired_curvature)
|
||||
return safe_desired_curvature
|
||||
|
||||
@@ -21,6 +21,7 @@ def similarity(s1: str, s2: str) -> float:
|
||||
|
||||
|
||||
def get_nn_model_path(CP: structs.CarParams) -> tuple[str, str, bool]:
|
||||
exact_match = True
|
||||
car_fingerprint = CP.carFingerprint
|
||||
eps_fw = str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), ""))
|
||||
|
||||
@@ -43,14 +44,11 @@ def get_nn_model_path(CP: structs.CarParams) -> tuple[str, str, bool]:
|
||||
nn_candidate = car_fingerprint
|
||||
|
||||
model_path, max_similarity = check_nn_path(nn_candidate)
|
||||
exact_match = max_similarity >= 0.99
|
||||
|
||||
if car_fingerprint not in model_path or 0.0 <= max_similarity < 0.9:
|
||||
if car_fingerprint not in model_path or 0.0 <= max_similarity < 0.8:
|
||||
nn_candidate = car_fingerprint
|
||||
model_path, max_similarity = check_nn_path(nn_candidate)
|
||||
exact_match = max_similarity >= 0.99
|
||||
|
||||
if 0.0 <= max_similarity < 0.9:
|
||||
if 0.0 <= max_similarity < 0.8:
|
||||
with open(TORQUE_NN_MODEL_SUBSTITUTE_PATH, 'rb') as f:
|
||||
sub = tomllib.load(f)
|
||||
sub_candidate = sub.get(car_fingerprint, car_fingerprint)
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestNNLCFingerprintBase:
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
|
||||
sunnypilot_interfaces.setup_interfaces(CP, CP_SP, Params())
|
||||
sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP, Params())
|
||||
|
||||
return CI
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from numpy.ma.testutils import assert_equal
|
||||
from parameterized import parameterized
|
||||
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
@@ -22,7 +23,7 @@ class TestNNTorqueModel:
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
|
||||
CI = CarInterface(CP, CP_SP)
|
||||
|
||||
sunnypilot_interfaces.setup_interfaces(CP, CP_SP, params)
|
||||
sunnypilot_interfaces.setup_car_interface_sp(CP, CP_SP, params)
|
||||
|
||||
CP_SP = convert_to_capnp(CP_SP)
|
||||
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
"""
|
||||
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 parameterized import parameterized
|
||||
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \
|
||||
AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY
|
||||
|
||||
AUTO_LANE_CHANGE_TIMER_COMBOS = [
|
||||
(AutoLaneChangeMode.NUDGELESS, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]),
|
||||
(AutoLaneChangeMode.HALF_SECOND, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.HALF_SECOND]),
|
||||
(AutoLaneChangeMode.ONE_SECOND, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.ONE_SECOND]),
|
||||
(AutoLaneChangeMode.TWO_SECONDS, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.TWO_SECONDS]),
|
||||
(AutoLaneChangeMode.THREE_SECONDS, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.THREE_SECONDS])
|
||||
]
|
||||
|
||||
|
||||
class TestAutoLaneChangeController:
|
||||
def setup_method(self):
|
||||
self.DH = DesireHelper()
|
||||
self.alc = AutoLaneChangeController(self.DH)
|
||||
|
||||
def _reset_states(self):
|
||||
self.alc.lane_change_bsm_delay = False
|
||||
self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||
self.lane_change_wait_timer = 0.0
|
||||
self.prev_brake_pressed = False
|
||||
self.prev_lane_change = False
|
||||
|
||||
def test_reset(self):
|
||||
"""Test that reset correctly sets timers back to default."""
|
||||
# Set some non-default values
|
||||
self.alc.lane_change_wait_timer = 2.0
|
||||
self.alc.prev_brake_pressed = True
|
||||
|
||||
# Set the DesireHelper to trigger a reset
|
||||
self.DH.lane_change_state = LaneChangeState.off
|
||||
self.DH.lane_change_direction = LaneChangeDirection.none
|
||||
|
||||
# Call reset
|
||||
self.alc.reset()
|
||||
|
||||
# Check values were reset
|
||||
assert self.alc.lane_change_wait_timer == 0.0
|
||||
assert not self.alc.prev_brake_pressed
|
||||
|
||||
@parameterized.expand([(AutoLaneChangeMode.OFF, ), (AutoLaneChangeMode.NUDGE, )])
|
||||
def test_off_and_nudge_mode(self, timer_state):
|
||||
"""Test the default OFF and NUDGE mode behavior."""
|
||||
self._reset_states()
|
||||
# Setup mode
|
||||
self.alc.lane_change_bsm_delay = False # BSM delay off
|
||||
self.alc.lane_change_set_timer = timer_state
|
||||
|
||||
# Update controller
|
||||
num_updates = int(5.0 / DT_MDL)
|
||||
for _ in range(num_updates): # Run for 5 seconds
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
# Mode should not allow lane change immediately
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
|
||||
def test_nudgeless_mode(self):
|
||||
"""Test the NUDGELESS mode behavior."""
|
||||
self._reset_states()
|
||||
# Setup NUDGELESS mode
|
||||
self.alc.lane_change_bsm_delay = False # BSM delay off
|
||||
self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
|
||||
|
||||
# Update controller once to read params
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
# Update multiple times to exceed the timer threshold
|
||||
for _ in range(1): # Should exceed 0.1s with multiple DT_MDL updates
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
# Now lane change should be allowed
|
||||
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
||||
assert self.alc.auto_lane_change_allowed
|
||||
|
||||
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||
def test_timers(self, timer_state, timer_delay):
|
||||
self._reset_states()
|
||||
self.alc.lane_change_bsm_delay = False # BSM delay off
|
||||
self.alc.lane_change_set_timer = timer_state
|
||||
|
||||
# Update controller once
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
# The timer should still be below the threshold after one update
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
|
||||
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
||||
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
||||
for _ in range(num_updates):
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
# Now lane change should be allowed
|
||||
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
||||
assert self.alc.auto_lane_change_allowed
|
||||
|
||||
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||
def test_brake_pressed_disables_auto_lane_change(self, timer_state, timer_delay):
|
||||
"""Test that pressing the brake disables auto lane change."""
|
||||
self._reset_states()
|
||||
# Setup auto lane change mode
|
||||
self.alc.lane_change_bsm_delay = False
|
||||
self.alc.lane_change_set_timer = timer_state
|
||||
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
||||
|
||||
# Update with brake pressed for 1 second
|
||||
for _ in range(num_updates):
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=True)
|
||||
|
||||
# Even though it is an auto lane change mode, lane change should be disallowed due to brake pressed prior initiating lane change
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
|
||||
# Check that prev_brake_pressed is saved
|
||||
assert self.alc.prev_brake_pressed
|
||||
|
||||
# Even releasing brake shouldn't allow auto lane change
|
||||
for _ in range(num_updates):
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
|
||||
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||
def test_blindspot_detected_with_bsm_delay(self, timer_state, timer_delay):
|
||||
"""Test behavior when blindspot is detected with BSM delay enabled."""
|
||||
# Blindspot detected - should prevent auto lane change
|
||||
self._reset_states()
|
||||
self.alc.lane_change_bsm_delay = True # BSM delay on
|
||||
self.alc.lane_change_set_timer = timer_state
|
||||
|
||||
# Update with blindspot detected - this should prevent auto lane change
|
||||
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
|
||||
# Keep updating with blindspot detected - should still prevent auto lane change
|
||||
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
||||
for _ in range(num_updates):
|
||||
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
|
||||
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||
def test_blindspot_detected_then_undetected_with_bsm_delay(self, timer_state, timer_delay):
|
||||
"""Test behavior when blindspot is detected then undetected with BSM delay enabled."""
|
||||
# Blindspot clears - should allow auto lane change after sufficient time
|
||||
self._reset_states()
|
||||
self.alc.lane_change_bsm_delay = True
|
||||
self.alc.lane_change_set_timer = timer_state
|
||||
|
||||
# First update with blindspot detected to set the negative timer
|
||||
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
|
||||
# Now update with blindspot cleared - should start incrementing timer from negative value
|
||||
num_updates = int((timer_delay + abs(ONE_SECOND_DELAY)) / DT_MDL) + 1
|
||||
for _ in range(num_updates):
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
# After sufficient updates with no blindspot, auto lane change should be allowed
|
||||
assert self.alc.auto_lane_change_allowed
|
||||
|
||||
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||
def test_disallow_continuous_auto_lane_change(self, timer_state, timer_delay):
|
||||
self._reset_states()
|
||||
self.alc.lane_change_bsm_delay = False # BSM delay off
|
||||
self.alc.lane_change_set_timer = timer_state
|
||||
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
||||
|
||||
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
||||
for _ in range(num_updates):
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
# Now lane change should be allowed
|
||||
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
||||
assert self.alc.auto_lane_change_allowed
|
||||
|
||||
# Simulate lane change is initiated
|
||||
self.DH.lane_change_state = LaneChangeState.laneChangeStarting
|
||||
self.alc.update_state()
|
||||
|
||||
# Simulate lane change is completed, and one_blinker stays on
|
||||
self.DH.lane_change_state = LaneChangeState.preLaneChange
|
||||
self.alc.update_state()
|
||||
|
||||
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
||||
for _ in range(num_updates):
|
||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
||||
|
||||
assert not self.alc.auto_lane_change_allowed
|
||||
@@ -26,7 +26,7 @@ class EventsSP(EventsBase):
|
||||
return EVENT_NAME_SP[event]
|
||||
|
||||
def get_event_msg_type(self):
|
||||
return custom.OnroadEventSP.Event
|
||||
return custom.OnroadEventSP
|
||||
|
||||
|
||||
EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
|
||||
@@ -24,11 +24,11 @@ class SunnylinkApi(BaseApi):
|
||||
self.spinner = None
|
||||
self.params = Params()
|
||||
|
||||
def api_get(self, endpoint, method='GET', timeout=10, access_token=None, json=None, **kwargs):
|
||||
def api_get(self, endpoint, method='GET', timeout=10, access_token=None, **kwargs):
|
||||
if not self.params.get_bool("SunnylinkEnabled"):
|
||||
return None
|
||||
|
||||
return super().api_get(endpoint, method, timeout, access_token, json, **kwargs)
|
||||
return super().api_get(endpoint, method, timeout, access_token, **kwargs)
|
||||
|
||||
def resume_queued(self, timeout=10, **kwargs):
|
||||
sunnylinkId, commaId = self._resolve_dongle_ids()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
"""
|
||||
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 Crypto.Cipher import AES
|
||||
|
||||
|
||||
class AESCipher:
|
||||
def __init__(self, key: bytes, iv: bytes):
|
||||
if len(key) not in (16, 32):
|
||||
raise ValueError("Key must be 16 bytes (AES-128) or 32 bytes (AES-256).")
|
||||
if len(iv) != 16:
|
||||
raise ValueError("IV must be 16 bytes.")
|
||||
|
||||
self.key = key
|
||||
self.iv = iv
|
||||
|
||||
def encrypt(self, data: bytes) -> bytes:
|
||||
block_size = 16
|
||||
padding_length = block_size - (len(data) % block_size)
|
||||
padding = bytes([padding_length]) * padding_length
|
||||
padded_data = data + padding
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
return cipher.encrypt(padded_data)
|
||||
|
||||
def decrypt(self, encrypted_data: bytes) -> bytes:
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
decrypted_data = cipher.decrypt(encrypted_data)
|
||||
padding_length = decrypted_data[-1]
|
||||
return decrypted_data[:-padding_length]
|
||||
@@ -1,268 +0,0 @@
|
||||
"""
|
||||
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 base64
|
||||
import json
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from openpilot.common.git import get_branch
|
||||
from openpilot.common.params import Params, ParamKeyType
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.version import get_version
|
||||
|
||||
from cereal import messaging, custom
|
||||
from sunnypilot.sunnylink.api import SunnylinkApi
|
||||
from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder
|
||||
|
||||
|
||||
class OperationType(Enum):
|
||||
BACKUP = "backup"
|
||||
RESTORE = "restore"
|
||||
|
||||
|
||||
class BackupManagerSP:
|
||||
"""Manages device configuration backups to/from sunnylink"""
|
||||
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.device_id = self.params.get("SunnylinkDongleId", encoding="utf8")
|
||||
self.api = SunnylinkApi(self.device_id)
|
||||
self.pm = messaging.PubMaster(["backupManagerSP"])
|
||||
|
||||
# Status tracking
|
||||
self.backup_status = custom.BackupManagerSP.Status.idle
|
||||
self.restore_status = custom.BackupManagerSP.Status.idle
|
||||
|
||||
# Unified progress & operation type (only one operation runs at a time)
|
||||
self.progress = 0.0
|
||||
self.operation: OperationType | None = None
|
||||
|
||||
self.last_error = ""
|
||||
|
||||
def _report_status(self) -> None:
|
||||
"""Reports current backup manager state through the messaging system."""
|
||||
msg = messaging.new_message('backupManagerSP', valid=True)
|
||||
backup_state = msg.backupManagerSP
|
||||
|
||||
backup_state.backupStatus = self.backup_status
|
||||
backup_state.restoreStatus = self.restore_status
|
||||
# Both progress fields use the unified progress value
|
||||
backup_state.backupProgress = self.progress
|
||||
backup_state.restoreProgress = self.progress
|
||||
backup_state.lastError = self.last_error
|
||||
|
||||
# Optionally, add a field for operation type if supported:
|
||||
# backup_state.operationType = self.operation.value if self.operation else "none"
|
||||
|
||||
self.pm.send('backupManagerSP', msg)
|
||||
|
||||
def _update_progress(self, progress: float, op_type: OperationType) -> None:
|
||||
"""Updates the unified progress and operation type, then reports status."""
|
||||
self.progress = progress
|
||||
self.operation = op_type
|
||||
self._report_status()
|
||||
|
||||
def _collect_config_data(self) -> dict[str, Any]:
|
||||
"""Collects configuration data to be backed up."""
|
||||
config_data = {}
|
||||
params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)]
|
||||
for param in params_to_backup:
|
||||
value = self.params.get(param)
|
||||
if value is not None:
|
||||
config_data[param] = base64.b64encode(value).decode('utf-8')
|
||||
return config_data
|
||||
|
||||
def _get_metadata_value(self, metadata_list, key, default_value=None):
|
||||
return next((entry.get("value") for entry in metadata_list if entry.get("key") == key), default_value)
|
||||
|
||||
async def create_backup(self) -> bool:
|
||||
"""Creates and uploads a new backup to sunnylink."""
|
||||
try:
|
||||
self.backup_status = custom.BackupManagerSP.Status.inProgress
|
||||
self._update_progress(0.0, OperationType.BACKUP)
|
||||
|
||||
# Collect configuration data
|
||||
config_data = self._collect_config_data()
|
||||
self._update_progress(25.0, OperationType.BACKUP)
|
||||
|
||||
# Serialize and encrypt config data
|
||||
config_json = json.dumps(config_data)
|
||||
encrypted_config = encrypt_compress_data(config_json, use_aes_256=True)
|
||||
self._update_progress(50.0, OperationType.BACKUP)
|
||||
|
||||
backup_info = custom.BackupManagerSP.BackupInfo()
|
||||
backup_info.deviceId = self.device_id
|
||||
backup_info.config = encrypted_config
|
||||
backup_info.isEncrypted = True
|
||||
backup_info.createdAt = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
|
||||
backup_info.updatedAt = backup_info.createdAt
|
||||
backup_info.sunnypilotVersion = self._get_current_version()
|
||||
backup_info.backupMetadata = [
|
||||
custom.BackupManagerSP.MetadataEntry(key="creator", value="BackupManagerSP"),
|
||||
custom.BackupManagerSP.MetadataEntry(key="all_values_encoded", value="True"),
|
||||
custom.BackupManagerSP.MetadataEntry(key="AES", value="256")
|
||||
]
|
||||
|
||||
payload = json.loads(json.dumps(backup_info.to_dict(), cls=SnakeCaseEncoder))
|
||||
self._update_progress(75.0, OperationType.BACKUP)
|
||||
|
||||
# Upload to sunnylink
|
||||
result = self.api.api_get(
|
||||
f"backup/{self.device_id}",
|
||||
method='PUT',
|
||||
access_token=self.api.get_token(),
|
||||
json=payload
|
||||
)
|
||||
|
||||
if result:
|
||||
self.backup_status = custom.BackupManagerSP.Status.completed
|
||||
self._update_progress(100.0, OperationType.BACKUP)
|
||||
else:
|
||||
self.backup_status = custom.BackupManagerSP.Status.failed
|
||||
self.last_error = "Failed to upload backup"
|
||||
self._report_status()
|
||||
|
||||
return bool(self.backup_status == custom.BackupManagerSP.Status.completed)
|
||||
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error creating backup: {str(e)}")
|
||||
self.backup_status = custom.BackupManagerSP.Status.failed
|
||||
self.last_error = str(e)
|
||||
self._report_status()
|
||||
return False
|
||||
|
||||
async def restore_backup(self, version: int | None = None) -> bool:
|
||||
"""Restores a backup from sunnylink."""
|
||||
try:
|
||||
self.restore_status = custom.BackupManagerSP.Status.inProgress
|
||||
self._update_progress(0.0, OperationType.RESTORE)
|
||||
|
||||
# Get backup data from API for the specified version
|
||||
endpoint = f"backup/{self.device_id}" + f"/{version or ''}" + "?api-version=1"
|
||||
backup_data = self.api.api_get(endpoint, access_token=self.api.get_token())
|
||||
if not backup_data:
|
||||
raise Exception(f"No backup found for device {self.device_id}")
|
||||
|
||||
self._update_progress(25.0, OperationType.RESTORE)
|
||||
|
||||
data = backup_data.json()
|
||||
backup_metadata = data.get("backup_metadata", [])
|
||||
encrypted_config = data.get("config", "")
|
||||
if not encrypted_config:
|
||||
raise Exception("Empty backup configuration")
|
||||
self._update_progress(50.0, OperationType.RESTORE)
|
||||
|
||||
# Decrypt config and load data
|
||||
use_aes_256 = self._get_metadata_value(backup_metadata, "AES", "128") == "256"
|
||||
config_json = decrypt_compressed_data(encrypted_config, use_aes_256)
|
||||
if not config_json:
|
||||
raise Exception("Failed to decrypt backup configuration")
|
||||
|
||||
config_data = json.loads(config_json)
|
||||
self._update_progress(75.0, OperationType.RESTORE)
|
||||
|
||||
# Apply configuration
|
||||
all_values_encoded = self._get_metadata_value(backup_metadata, "all_values_encoded", "false")
|
||||
self._apply_config(config_data, str(all_values_encoded).lower() == "true")
|
||||
|
||||
self.restore_status = custom.BackupManagerSP.Status.completed
|
||||
self._update_progress(100.0, OperationType.RESTORE)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error restoring backup: {str(e)}")
|
||||
self.restore_status = custom.BackupManagerSP.Status.failed
|
||||
self.last_error = str(e)
|
||||
self._report_status()
|
||||
return False
|
||||
|
||||
def _apply_config(self, config_data: dict[str, str], all_values_encoded: bool = False) -> None:
|
||||
"""Applies configuration data from a backup, but only for parameters marked as backupable."""
|
||||
# Get the current list of parameters that can be backed up
|
||||
backupable_params = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)]
|
||||
|
||||
# Count for logging/reporting
|
||||
restored_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for param, encoded_value in config_data.items():
|
||||
try:
|
||||
# Only restore parameters that are currently marked as backupable
|
||||
if param in backupable_params:
|
||||
value = base64.b64decode(encoded_value) if all_values_encoded else encoded_value
|
||||
self.params.put(param, value)
|
||||
restored_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
cloudlog.info(f"Skipped restoring param {param}: not marked for backup in current version")
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to restore param {param}: {str(e)}")
|
||||
|
||||
cloudlog.info(f"Restore complete: {restored_count} params restored, {skipped_count} params skipped")
|
||||
|
||||
def _get_current_version(self) -> custom.BackupManagerSP.Version:
|
||||
"""Gets current sunnypilot version information."""
|
||||
version_obj = custom.BackupManagerSP.Version()
|
||||
version_parts = get_version().split('.')
|
||||
version_obj.major = int(version_parts[0]) if len(version_parts) > 0 else 0
|
||||
version_obj.minor = int(version_parts[1]) if len(version_parts) > 1 else 0
|
||||
version_obj.patch = int(version_parts[2]) if len(version_parts) > 2 else 0
|
||||
version_obj.build = int(version_parts[3]) if len(version_parts) > 3 else 0
|
||||
version_obj.branch = get_branch()
|
||||
return version_obj
|
||||
|
||||
async def main_thread(self) -> None:
|
||||
"""Main thread for backup management."""
|
||||
rk = Ratekeeper(1, print_delay_threshold=None)
|
||||
reset_progress = False
|
||||
|
||||
while True:
|
||||
try:
|
||||
if reset_progress:
|
||||
self.progress = 100.0
|
||||
self.operation = None
|
||||
self.restore_status = custom.BackupManagerSP.Status.idle
|
||||
self.backup_status = custom.BackupManagerSP.Status.idle
|
||||
|
||||
# Check for backup command
|
||||
if self.params.get_bool("BackupManager_CreateBackup"):
|
||||
try:
|
||||
await self.create_backup()
|
||||
reset_progress = True
|
||||
finally:
|
||||
self.params.remove("BackupManager_CreateBackup")
|
||||
|
||||
# Check for restore command
|
||||
restore_version = self.params.get("BackupManager_RestoreVersion", encoding="utf8")
|
||||
if restore_version:
|
||||
try:
|
||||
version = int(restore_version) if restore_version.isdigit() else None
|
||||
await self.restore_backup(version)
|
||||
reset_progress = True
|
||||
finally:
|
||||
self.params.remove("BackupManager_RestoreVersion")
|
||||
|
||||
self._report_status()
|
||||
rk.keep_time()
|
||||
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error in backup manager main thread: {str(e)}")
|
||||
self.last_error = str(e)
|
||||
self._report_status()
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
def main():
|
||||
import asyncio
|
||||
asyncio.run(BackupManagerSP().main_thread())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,170 +0,0 @@
|
||||
"""
|
||||
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 base64
|
||||
import hashlib
|
||||
import zlib
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
from sunnypilot.sunnylink.backups.AESCipher import AESCipher
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
|
||||
class KeyDerivation:
|
||||
@staticmethod
|
||||
def _load_key(file_path: str) -> bytes:
|
||||
with open(file_path, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
@staticmethod
|
||||
def derive_aes_key_iv_from_rsa(key_path: str, use_aes_256: bool) -> tuple[bytes, bytes]:
|
||||
rsa_key_pem: bytes = KeyDerivation._load_key(key_path)
|
||||
key_plain = rsa_key_pem.decode(errors="ignore")
|
||||
|
||||
if "private" in key_plain.lower():
|
||||
private_key = serialization.load_pem_private_key(rsa_key_pem, password=None, backend=default_backend())
|
||||
if not isinstance(private_key, rsa.RSAPrivateKey):
|
||||
raise ValueError("Invalid RSA key format: Unable to determine if key is public or private.")
|
||||
|
||||
der_data = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.DER,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
elif "public" in key_plain.lower():
|
||||
public_key = serialization.load_pem_public_key(rsa_key_pem, backend=default_backend())
|
||||
if not isinstance(public_key, rsa.RSAPublicKey):
|
||||
raise ValueError("Invalid RSA key format: Unable to determine if key is public or private.")
|
||||
|
||||
der_data = public_key.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.PKCS1)
|
||||
else:
|
||||
raise ValueError("Unknown key format: Unable to determine if key is public or private.")
|
||||
|
||||
sha256_hash = hashlib.sha256(der_data).digest()
|
||||
aes_key = sha256_hash[:32] if use_aes_256 else sha256_hash[:16]
|
||||
aes_iv = sha256_hash[16:32]
|
||||
|
||||
return aes_key, aes_iv
|
||||
|
||||
|
||||
def qUncompress(data):
|
||||
"""
|
||||
Decompress data using zlib.
|
||||
|
||||
Args:
|
||||
data (bytes): Compressed data
|
||||
|
||||
Returns:
|
||||
bytes: Decompressed data
|
||||
"""
|
||||
data_stripped_4 = data[4:]
|
||||
return zlib.decompress(data_stripped_4)
|
||||
|
||||
|
||||
def qCompress(data):
|
||||
"""
|
||||
Compress data using zlib.
|
||||
|
||||
Args:
|
||||
data (bytes): Data to compress
|
||||
|
||||
Returns:
|
||||
bytes: Compressed data
|
||||
"""
|
||||
compressed_data = zlib.compress(data, level=9)
|
||||
return b"ZLIB" + compressed_data
|
||||
|
||||
|
||||
def decrypt_compressed_data(encrypted_base64, use_aes_256=False):
|
||||
"""
|
||||
Decrypt and decompress data from base64 string.
|
||||
|
||||
Args:
|
||||
encrypted_base64 (str): Base64 encoded encrypted data
|
||||
key_path (str, optional): Path to RSA public key
|
||||
|
||||
Returns:
|
||||
str: Decrypted and decompressed string
|
||||
"""
|
||||
key_path = Path(f"{Paths.persist_root()}/comma/id_rsa") if use_aes_256 else Path(f"{Paths.persist_root()}/comma/id_rsa.pub")
|
||||
try:
|
||||
# Decode base64
|
||||
encrypted_data = base64.b64decode(encrypted_base64)
|
||||
|
||||
# Decrypt
|
||||
key, iv = KeyDerivation.derive_aes_key_iv_from_rsa(str(key_path), use_aes_256)
|
||||
cipher = AESCipher(key, iv)
|
||||
decrypted_data = cipher.decrypt(encrypted_data)
|
||||
|
||||
# Decompress
|
||||
decompressed_data = qUncompress(decrypted_data)
|
||||
|
||||
# Decode UTF-8
|
||||
result = decompressed_data.decode('utf-8')
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Decryption and decompression failed: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def encrypt_compress_data(text, use_aes_256=True):
|
||||
"""
|
||||
Compress and encrypt string data to base64.
|
||||
|
||||
Args:
|
||||
text (str): Text to compress and encrypt
|
||||
key_path (str, optional): Path to RSA public key
|
||||
|
||||
Returns:
|
||||
str: Base64 encoded encrypted data
|
||||
"""
|
||||
key_path = Path(f"{Paths.persist_root()}/comma/id_rsa") if use_aes_256 else Path(f"{Paths.persist_root()}/comma/id_rsa.pub")
|
||||
try:
|
||||
# Encode to UTF-8
|
||||
text_bytes = text.encode('utf-8')
|
||||
|
||||
# Compress
|
||||
compressed_data = qCompress(text_bytes)
|
||||
|
||||
# Encrypt
|
||||
key, iv = KeyDerivation.derive_aes_key_iv_from_rsa(str(key_path), use_aes_256)
|
||||
cipher = AESCipher(key, iv)
|
||||
encrypted_data = cipher.encrypt(compressed_data)
|
||||
|
||||
# Encode to base64
|
||||
result = base64.b64encode(encrypted_data).decode('utf-8')
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Compression and encryption failed: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def camel_to_snake(name):
|
||||
"""Convert camelCase to snake_case."""
|
||||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
|
||||
|
||||
|
||||
def transform_dict(obj):
|
||||
"""Recursively transform dictionary keys from camelCase to snake_case."""
|
||||
if isinstance(obj, dict):
|
||||
return {camel_to_snake(k): transform_dict(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [transform_dict(item) for item in obj]
|
||||
return obj
|
||||
|
||||
|
||||
class SnakeCaseEncoder(json.JSONEncoder):
|
||||
def encode(self, obj):
|
||||
transformed_obj = transform_dict(obj)
|
||||
return super().encode(transformed_obj)
|
||||
@@ -91,7 +91,6 @@ def register(show_spinner=False) -> str | None:
|
||||
|
||||
if time.monotonic() - start_time > 60 and show_spinner:
|
||||
spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})")
|
||||
return UNREGISTERED_DONGLE_ID # hotfix to prevent an infinite wait for registration
|
||||
|
||||
if show_spinner:
|
||||
spinner.close()
|
||||
|
||||
@@ -43,8 +43,6 @@ def manager_init() -> None:
|
||||
]
|
||||
|
||||
sunnypilot_default_params: list[tuple[str, str | bytes]] = [
|
||||
("AutoLaneChangeTimer", "0"),
|
||||
("AutoLaneChangeBsmDelay", "0"),
|
||||
("DynamicExperimentalControl", "0"),
|
||||
("Mads", "1"),
|
||||
("MadsMainCruiseAllowed", "1"),
|
||||
@@ -53,7 +51,6 @@ def manager_init() -> None:
|
||||
("ModelManager_LastSyncTime", "0"),
|
||||
("ModelManager_ModelsCache", ""),
|
||||
("NeuralNetworkLateralControl", "0"),
|
||||
("QuietMode", "0"),
|
||||
]
|
||||
|
||||
if params.get_bool("RecordFrontLock"):
|
||||
|
||||
@@ -148,13 +148,9 @@ procs = [
|
||||
|
||||
# sunnypilot
|
||||
procs += [
|
||||
# Models
|
||||
PythonProcess("models_manager", "sunnypilot.models.manager", only_offroad),
|
||||
NativeProcess("modeld_snpe", "sunnypilot/modeld", ["./modeld"], and_(only_onroad, is_snpe_model)),
|
||||
NativeProcess("modeld_tinygrad", "sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)),
|
||||
|
||||
# Backup
|
||||
PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
|
||||
]
|
||||
|
||||
if os.path.exists("./github_runner.sh"):
|
||||
|
||||
Reference in New Issue
Block a user