mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-08 02:05:43 +08:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e0d89968d | |||
| fec5a97a9e | |||
| 099143ad9d | |||
| 6909aa95ff | |||
| 5bdc0c23a9 | |||
| 1a07e47228 | |||
| 50b860c928 | |||
| 978ec800fe | |||
| 3a05c03079 | |||
| fd22de1c9a | |||
| ee3583df33 | |||
| 7801bdf0cc | |||
| 0265ae5f76 | |||
| 9163d1cb74 | |||
| a0cc313fdc | |||
| 93bb596a2e | |||
| 41b14fa595 | |||
| fdd1df79fb | |||
| 031b1ad0a3 | |||
| b1f2e638de | |||
| c20263d985 | |||
| 5472e69e35 | |||
| 78909dac73 | |||
| 24a9b6dae5 | |||
| d9596fa998 | |||
| e124d6df9b | |||
| 3dbf02f803 | |||
| fef29ad225 | |||
| 157c7080ce | |||
| caa9e770cc | |||
| 24893ebadb | |||
| 39e12c8bb1 | |||
| f0d93eb32d | |||
| 19ecc37de8 | |||
| ecac2d386b | |||
| e475d10adc | |||
| 7d74c3c99b | |||
| cefcf10ec3 | |||
| 1c07e00759 | |||
| f0841b827a | |||
| 3f49e2d33c | |||
| 3f93b00120 | |||
| c21b0821da | |||
| b9f25f8a43 | |||
| c8786d930d | |||
| 3a55f31dc5 | |||
| a04c045cd7 | |||
| 9aa5c3d2c6 | |||
| 5d23a78c77 | |||
| 06a73f538e | |||
| 60716edc37 | |||
| 61608db786 |
@@ -12,11 +12,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
recompiled_dir:
|
||||
description: 'Existing recompiled directory number (e.g. 3 for recompiled3)'
|
||||
description: 'Existing recompiled directory number (e.g. 1 for recompiled1)'
|
||||
required: true
|
||||
type: string
|
||||
json_version:
|
||||
description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)'
|
||||
description: 'driving_models version number to update (e.g. 18 for driving_models_v18.json)'
|
||||
required: true
|
||||
type: string
|
||||
artifact_suffix:
|
||||
@@ -63,12 +63,11 @@ on:
|
||||
default: 'None'
|
||||
options:
|
||||
- None
|
||||
- Simple Plan Models
|
||||
- Space Lab Models
|
||||
- TR Models
|
||||
- DTR Models
|
||||
- Master Models
|
||||
- Release Models
|
||||
- 2025 World Models
|
||||
- 2026 World Models
|
||||
- Custom Merge Models
|
||||
- FOF series models
|
||||
- Other
|
||||
custom_model_folder:
|
||||
description: 'Custom model folder name (if "Other" selected)'
|
||||
|
||||
@@ -65,8 +65,21 @@ jobs:
|
||||
echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig
|
||||
echo ' locksverify = false' >> .lfsconfig
|
||||
|
||||
- name: Configure LFS transfer settings
|
||||
run: |
|
||||
git config lfs.activitytimeout 300
|
||||
git config lfs.transfer.maxretries 5
|
||||
git config lfs.concurrenttransfers 4
|
||||
|
||||
- name: Push LFS
|
||||
id: sync-and-commit
|
||||
run: |
|
||||
git lfs ls-files -l
|
||||
git lfs push --all origin
|
||||
for attempt in 1 2 3; do
|
||||
echo "Push attempt $attempt..."
|
||||
git lfs push --all origin && exit 0
|
||||
echo "Attempt $attempt failed, retrying in 30s..."
|
||||
sleep 30
|
||||
done
|
||||
echo "All push attempts failed"
|
||||
exit 1
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
name: prebuilt
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD: release/ci/docker_build_sp.sh
|
||||
|
||||
jobs:
|
||||
build_prebuilt:
|
||||
name: build prebuilt
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'sunnypilot/sunnypilot'
|
||||
env:
|
||||
PUSH_IMAGE: true
|
||||
permissions:
|
||||
checks: read
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Wait for green check mark
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
|
||||
with:
|
||||
ref: master
|
||||
wait-interval: 30
|
||||
running-workflow-name: 'build prebuilt'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
check-regexp: ^((?!.*(build master-ci|create badges).*).)*$
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- run: git lfs pull
|
||||
- name: Build and Push docker image
|
||||
run: |
|
||||
$DOCKER_LOGIN
|
||||
eval "$BUILD"
|
||||
@@ -93,6 +93,14 @@ jobs:
|
||||
const { owner, repo } = context.repo;
|
||||
const upstream = `${owner}/${repo}`;
|
||||
|
||||
const closed = context.payload.pull_request;
|
||||
if (closed) {
|
||||
if (closed.head.repo?.full_name === upstream) {
|
||||
await github.rest.git.deleteRef({ owner, repo, ref: `heads/${closed.head.ref}` }).catch(console.log);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for await (const response of github.paginate.iterator(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
|
||||
@@ -30,6 +30,11 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
target_hardware:
|
||||
description: 'Hardware target to compile for (qcom or usbgpu)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'qcom'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upstream_branch:
|
||||
@@ -46,6 +51,14 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
target_hardware:
|
||||
description: 'Hardware target to compile for'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- qcom
|
||||
- usbgpu
|
||||
default: 'qcom'
|
||||
|
||||
|
||||
run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}]
|
||||
@@ -161,19 +174,30 @@ jobs:
|
||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||
path: ${{ env.MODELS_DIR }}
|
||||
- run: |
|
||||
rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision}.onnx
|
||||
rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision,big_driving_supercombo}.onnx
|
||||
|
||||
- name: Build Model
|
||||
run: |
|
||||
source /etc/profile
|
||||
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
|
||||
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
export PYTHONPATH="${PYTHONPATH}:${{ env.TINYGRAD_PATH }}:${{ github.workspace }}"
|
||||
|
||||
COMPILE_MODELD="${{ github.workspace }}/openpilot/sunnypilot/modeld_v2/compile_modeld.py"
|
||||
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
|
||||
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
|
||||
echo "USBGPU build"
|
||||
export USBGPU=1
|
||||
TG_FLAGS="DEV=AMD USBGPU=1 IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
||||
else
|
||||
echo "QCOM build"
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
|
||||
fi
|
||||
|
||||
# Generate metadata for all ONNX files
|
||||
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
|
||||
@@ -186,7 +210,13 @@ jobs:
|
||||
POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx"
|
||||
OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx"
|
||||
ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx"
|
||||
SUPERCOMBO_ONNX="${{ env.MODELS_DIR }}/supercombo.onnx"
|
||||
SUPERCOMBO_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx"; do
|
||||
if [ -f "$f" ]; then
|
||||
SUPERCOMBO_ONNX="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
||||
if [ -f "$VISION_ONNX" ]; then
|
||||
@@ -207,24 +237,15 @@ jobs:
|
||||
fi
|
||||
|
||||
if [ -n "$MODEL_TYPE" ]; then
|
||||
echo "Detected: $MODEL_TYPE -> driving_tinygrad.pkl"
|
||||
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
|
||||
env ${TG_FLAGS} python3 "$COMPILE_MODELD" \
|
||||
--model-type $MODEL_TYPE \
|
||||
--model-size $MODEL_SIZE \
|
||||
--camera-resolutions $CAMERA_RES \
|
||||
$ONNX_ARGS \
|
||||
--output "${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
|
||||
--output "$OUTPUT_PKL"
|
||||
fi
|
||||
|
||||
- name: Validate Model Outputs
|
||||
run: |
|
||||
source /etc/profile
|
||||
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
|
||||
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
|
||||
python3 "${{ github.workspace }}/release/ci/model_generator.py" \
|
||||
--validate-only \
|
||||
--model-dir "${{ env.MODELS_DIR }}"
|
||||
|
||||
- name: Prepare Output
|
||||
run: |
|
||||
sudo rm -rf ${{ env.OUTPUT_DIR }}
|
||||
|
||||
@@ -166,6 +166,11 @@ env = Environment(
|
||||
tools=["default", "cython", "compilation_db", "rednose_filter"],
|
||||
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
|
||||
)
|
||||
# SCons' Darwin linker tool doesn't define the variables used to expand RPATH.
|
||||
if arch == "Darwin":
|
||||
env["RPATHPREFIX"] = "-Wl,-rpath,"
|
||||
env["RPATHSUFFIX"] = ""
|
||||
env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}"
|
||||
if arch != "larch64":
|
||||
env['_LIBFLAGS'] = _libflags
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# AI policy
|
||||
|
||||
## Why this exists
|
||||
|
||||
We use AI tools ourselves, so this isn't an anti-AI stance. The problem is people submitting code, issues, or comments they don't actually understand. AI makes that very easy to do, and it creates real work for reviewers who have to figure out what you meant when you can't explain it yourself.
|
||||
|
||||
If you're not going to put effort into understanding and verifying your submission, we're not going to put effort into reviewing it.
|
||||
|
||||
## The rule
|
||||
|
||||
You are responsible for everything you submit: code, PR descriptions, issues, bug reports, comments.
|
||||
|
||||
1. Understand what you submit. If a reviewer asks why you did something, you answer from your own understanding, not by re-prompting. If you can't do that, don't submit it.
|
||||
|
||||
2. Test your change. AI gets things wrong all the time. Run it, break it, confirm it actually works.
|
||||
|
||||
3. Driving fixes need real evidence. Attach a dongle ID, upload logs, and include segments that show the fix working. A route hash by itself proves nothing.
|
||||
|
||||
4. No AI-generated media (images, diagrams, videos) in issues or PRs.
|
||||
|
||||
## Disclosure
|
||||
|
||||
If AI tools helped you write something, say so. Add an `Assisted-by:` line in your commit message:
|
||||
|
||||
```
|
||||
Assisted-by: GitHub Copilot
|
||||
Assisted-by: Claude
|
||||
```
|
||||
|
||||
Disclosing won't count against your PR. It helps reviewers know where to look. Hiding it and getting caught will.
|
||||
|
||||
## How we review
|
||||
|
||||
Reviewers are looking at whether you understand your own change. Can you explain it? Can you respond to feedback without re-prompting? Does your PR description say why you made the change, not just list what changed?
|
||||
|
||||
Good code from someone who used AI and understands what they wrote is fine. How you got there doesn't matter as long as you can stand behind it.
|
||||
|
||||
## What happens
|
||||
|
||||
Submissions that don't meet this bar get closed. If it keeps happening, you get blocked.
|
||||
|
||||
## Maintainers
|
||||
|
||||
Maintainers use AI at their discretion. They've earned that through sustained contribution and they know the codebase.
|
||||
@@ -1,3 +1,5 @@
|
||||
> sunnypilot follows [commaai/openpilot](https://github.com/commaai/openpilot)'s contributing guidelines. The following applies to all contributions here.
|
||||
|
||||
# How to contribute
|
||||
|
||||
Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about externalization](https://blog.comma.ai/a-2020-theme-externalization/).
|
||||
@@ -35,6 +37,7 @@ All of these are examples of good PRs:
|
||||
* **UI design**: we do not have a good review process for this yet
|
||||
* **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't.
|
||||
* **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged.
|
||||
* **AI-generated contributions**: see our [AI policy](AI_POLICY.md)
|
||||
|
||||
### First contribution
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="18.4"
|
||||
export AGNOS_VERSION="18.5"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 0ed33dae96...063414f63f
@@ -69,6 +69,8 @@ struct LeadData {
|
||||
struct SelfdriveStateSP @0x81c2f05a394cf4af {
|
||||
mads @0 :ModularAssistiveDrivingSystem;
|
||||
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
|
||||
buttonsPressed @2 :UInt16;
|
||||
buttonsReleaseToggle @3 :UInt16;
|
||||
|
||||
enum AudibleAlert {
|
||||
none @0;
|
||||
@@ -137,10 +139,16 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
eta @2 :UInt32;
|
||||
}
|
||||
|
||||
struct Chunk {
|
||||
fileName @0 :Text;
|
||||
sha256 @1 :Text;
|
||||
}
|
||||
|
||||
struct Artifact {
|
||||
fileName @0 :Text;
|
||||
downloadUri @1 :DownloadUri;
|
||||
downloadProgress @2 :DownloadProgress;
|
||||
chunks @3 :List(Chunk);
|
||||
}
|
||||
|
||||
struct Model {
|
||||
@@ -155,6 +163,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
policy @3;
|
||||
offPolicy @4;
|
||||
onPolicy @5;
|
||||
chunked @6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -812,6 +812,7 @@ struct SelfdriveState {
|
||||
promptDistracted @8;
|
||||
|
||||
preAlert @9;
|
||||
complete @10;
|
||||
}
|
||||
|
||||
enum OpenpilotState @0xdbe58b96d2d1ac61 {
|
||||
@@ -2160,7 +2161,8 @@ struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 {
|
||||
|
||||
struct DriverMonitoringState {
|
||||
lockout @0 :Bool;
|
||||
lockoutRecoveryPercent @11 :Int8;
|
||||
lockoutCount @15 :Int8;
|
||||
lockoutMinutesRemaining @11 :Int8;
|
||||
alert3Count @12 :Int8;
|
||||
noResponseCount @13 :Int8;
|
||||
noResponseForceDecel @14 :Bool;
|
||||
|
||||
@@ -7,12 +7,43 @@ import os
|
||||
import capnp
|
||||
import time
|
||||
|
||||
from typing import Optional, List, Union, Dict
|
||||
from typing import Union
|
||||
|
||||
from openpilot.cereal import log
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.common.utils import MovingAverage
|
||||
|
||||
__all__ = (
|
||||
"NO_TRAVERSAL_LIMIT",
|
||||
"Context",
|
||||
"FrequencyTracker",
|
||||
"IpcError",
|
||||
"MultiplePublishersError",
|
||||
"Poller",
|
||||
"PubMaster",
|
||||
"PubSocket",
|
||||
"SocketEventHandle",
|
||||
"SubMaster",
|
||||
"SubSocket",
|
||||
"delete_fake_prefix",
|
||||
"drain_sock",
|
||||
"drain_sock_raw",
|
||||
"fake_event_handle",
|
||||
"get_fake_prefix",
|
||||
"log_from_bytes",
|
||||
"new_message",
|
||||
"pub_sock",
|
||||
"recv_one",
|
||||
"recv_one_or_none",
|
||||
"recv_one_retry",
|
||||
"recv_sock",
|
||||
"reset_context",
|
||||
"set_fake_prefix",
|
||||
"sub_sock",
|
||||
"toggle_fake_events",
|
||||
"wait_for_one_event",
|
||||
)
|
||||
|
||||
NO_TRAVERSAL_LIMIT = 2**64-1
|
||||
|
||||
|
||||
@@ -22,8 +53,8 @@ def pub_sock(endpoint: str) -> PubSocket:
|
||||
return msgq.pub_sock(endpoint, segment_size)
|
||||
|
||||
|
||||
def sub_sock(endpoint: str, poller: Optional[Poller] = None, addr: str = "127.0.0.1",
|
||||
conflate: bool = False, timeout: Optional[int] = None) -> SubSocket:
|
||||
def sub_sock(endpoint: str, poller: Poller | None = None, addr: str = "127.0.0.1",
|
||||
conflate: bool = False, timeout: int | None = None) -> SubSocket:
|
||||
service = SERVICE_LIST.get(endpoint)
|
||||
segment_size = service.queue_size if service else 0
|
||||
return msgq.sub_sock(endpoint, poller=poller, addr=addr, conflate=conflate,
|
||||
@@ -39,7 +70,7 @@ def log_from_bytes(dat: bytes, struct: capnp.lib.capnp._StructModule = log.Event
|
||||
return msg
|
||||
|
||||
|
||||
def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder:
|
||||
def new_message(service: str | None, size: int | None = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder:
|
||||
args = {
|
||||
'valid': False,
|
||||
'logMonoTime': int(time.monotonic() * 1e9),
|
||||
@@ -54,14 +85,14 @@ def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) ->
|
||||
return dat
|
||||
|
||||
|
||||
def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> List[capnp.lib.capnp._DynamicStructReader]:
|
||||
def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> list[capnp.lib.capnp._DynamicStructReader]:
|
||||
"""Receive all message currently available on the queue"""
|
||||
msgs = drain_sock_raw(sock, wait_for_one=wait_for_one)
|
||||
return [log_from_bytes(m) for m in msgs]
|
||||
|
||||
|
||||
# TODO: print when we drop packets?
|
||||
def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._DynamicStructReader]:
|
||||
def recv_sock(sock: SubSocket, wait: bool = False) -> capnp.lib.capnp._DynamicStructReader | None:
|
||||
"""Same as drain sock, but only returns latest message. Consider using conflate instead."""
|
||||
dat = None
|
||||
|
||||
@@ -82,14 +113,14 @@ def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._
|
||||
return dat
|
||||
|
||||
|
||||
def recv_one(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]:
|
||||
def recv_one(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None:
|
||||
dat = sock.receive()
|
||||
if dat is not None:
|
||||
dat = log_from_bytes(dat)
|
||||
return dat
|
||||
|
||||
|
||||
def recv_one_or_none(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]:
|
||||
def recv_one_or_none(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None:
|
||||
dat = sock.receive(non_blocking=True)
|
||||
if dat is not None:
|
||||
dat = log_from_bytes(dat)
|
||||
@@ -148,27 +179,27 @@ class FrequencyTracker:
|
||||
|
||||
|
||||
class SubMaster:
|
||||
def __init__(self, services: List[str], poll: Optional[str] = None,
|
||||
ignore_alive: Optional[List[str]] = None, ignore_avg_freq: Optional[List[str]] = None,
|
||||
ignore_valid: Optional[List[str]] = None, addr: str = "127.0.0.1", frequency: Optional[float] = None):
|
||||
def __init__(self, services: list[str], poll: str | None = None,
|
||||
ignore_alive: list[str] | None = None, ignore_avg_freq: list[str] | None = None,
|
||||
ignore_valid: list[str] | None = None, addr: str = "127.0.0.1", frequency: float | None = None):
|
||||
self.frame = -1
|
||||
self.services = services
|
||||
self.seen = {s: False for s in services}
|
||||
self.updated = {s: False for s in services}
|
||||
self.recv_time = {s: 0. for s in services}
|
||||
self.recv_frame = {s: 0 for s in services}
|
||||
self.seen = dict.fromkeys(services, False)
|
||||
self.updated = dict.fromkeys(services, False)
|
||||
self.recv_time = dict.fromkeys(services, 0.0)
|
||||
self.recv_frame = dict.fromkeys(services, 0)
|
||||
self.sock = {}
|
||||
self.data = {}
|
||||
self.logMonoTime = {s: 0 for s in services}
|
||||
self.logMonoTime = dict.fromkeys(services, 0)
|
||||
|
||||
# zero-frequency / on-demand services are always alive and presumed valid; all others must pass checks
|
||||
on_demand = {s: SERVICE_LIST[s].frequency <= 1e-5 for s in services}
|
||||
self.static_freq_services = set(s for s in services if not on_demand[s])
|
||||
self.static_freq_services = {s for s in services if not on_demand[s]}
|
||||
self.alive = {s: on_demand[s] for s in services}
|
||||
self.freq_ok = {s: on_demand[s] for s in services}
|
||||
self.valid = {s: on_demand[s] for s in services}
|
||||
|
||||
self.freq_tracker: Dict[str, FrequencyTracker] = {}
|
||||
self.freq_tracker: dict[str, FrequencyTracker] = {}
|
||||
self.poller = Poller()
|
||||
polled_services = set([poll, ] if poll is not None else services)
|
||||
self.non_polled_services = set(services) - polled_services
|
||||
@@ -211,7 +242,7 @@ class SubMaster:
|
||||
msgs.append(recv_one_or_none(self.sock[s]))
|
||||
self.update_msgs(time.monotonic(), msgs)
|
||||
|
||||
def update_msgs(self, cur_time: float, msgs: List[capnp.lib.capnp._DynamicStructReader]) -> None:
|
||||
def update_msgs(self, cur_time: float, msgs: list[capnp.lib.capnp._DynamicStructReader]) -> None:
|
||||
self.frame += 1
|
||||
self.updated = dict.fromkeys(self.services, False)
|
||||
for msg in msgs:
|
||||
@@ -234,21 +265,21 @@ class SubMaster:
|
||||
self.alive[s] = (cur_time - self.recv_time[s]) < (10. / SERVICE_LIST[s].frequency) or (self.seen[s] and self.simulation)
|
||||
self.freq_ok[s] = self.freq_tracker[s].valid or self.simulation
|
||||
|
||||
def all_alive(self, service_list: Optional[List[str]] = None) -> bool:
|
||||
def all_alive(self, service_list: list[str] | None = None) -> bool:
|
||||
return all(self.alive[s] for s in (service_list or self.services) if s not in self.ignore_alive)
|
||||
|
||||
def all_freq_ok(self, service_list: Optional[List[str]] = None) -> bool:
|
||||
def all_freq_ok(self, service_list: list[str] | None = None) -> bool:
|
||||
return all(self.freq_ok[s] for s in (service_list or self.services) if self._check_avg_freq(s))
|
||||
|
||||
def all_valid(self, service_list: Optional[List[str]] = None) -> bool:
|
||||
def all_valid(self, service_list: list[str] | None = None) -> bool:
|
||||
return all(self.valid[s] for s in (service_list or self.services) if s not in self.ignore_valid)
|
||||
|
||||
def all_checks(self, service_list: Optional[List[str]] = None) -> bool:
|
||||
def all_checks(self, service_list: list[str] | None = None) -> bool:
|
||||
return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list)
|
||||
|
||||
|
||||
class PubMaster:
|
||||
def __init__(self, services: List[str]):
|
||||
def __init__(self, services: list[str]):
|
||||
self.sock = {}
|
||||
for s in services:
|
||||
self.sock[s] = pub_sock(s)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import capnp
|
||||
import multiprocessing
|
||||
import numbers
|
||||
@@ -6,7 +5,6 @@ import random
|
||||
import threading
|
||||
import time
|
||||
from openpilot.common.parameterized import parameterized
|
||||
import pytest
|
||||
|
||||
from openpilot.cereal import log
|
||||
from opendbc.car.structs import car
|
||||
@@ -24,10 +22,6 @@ def random_socks(num_socks=10):
|
||||
def random_bytes(length=1000):
|
||||
return bytes([random.randrange(0xFF) for _ in range(length)])
|
||||
|
||||
def zmq_sleep(t=1):
|
||||
if "ZMQ" in os.environ:
|
||||
time.sleep(t)
|
||||
|
||||
|
||||
# TODO: this should take any capnp struct and returrn a msg with random populated data
|
||||
def random_carstate():
|
||||
@@ -53,16 +47,6 @@ def delayed_send(delay, sock, dat):
|
||||
|
||||
|
||||
class TestMessaging:
|
||||
def setUp(self):
|
||||
# TODO: ZMQ tests are too slow; all sleeps will need to be
|
||||
# replaced with logic to block on the necessary condition
|
||||
if "ZMQ" in os.environ:
|
||||
pytest.skip()
|
||||
|
||||
# ZMQ pub socket takes too long to die
|
||||
# sleep to prevent multiple publishers error between tests
|
||||
zmq_sleep()
|
||||
|
||||
@parameterized.expand(events)
|
||||
def test_new_message(self, evt):
|
||||
try:
|
||||
@@ -89,7 +73,6 @@ class TestMessaging:
|
||||
sock = "carState"
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sub_sock = messaging.sub_sock(sock, timeout=1000)
|
||||
zmq_sleep()
|
||||
|
||||
# no wait and no msgs in queue
|
||||
msgs = func(sub_sock)
|
||||
@@ -110,7 +93,6 @@ class TestMessaging:
|
||||
sock = "carState"
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sub_sock = messaging.sub_sock(sock, timeout=100)
|
||||
zmq_sleep()
|
||||
|
||||
# no wait and no msg in queue, socket should timeout
|
||||
recvd = messaging.recv_sock(sub_sock)
|
||||
@@ -129,7 +111,6 @@ class TestMessaging:
|
||||
sock = "carState"
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sub_sock = messaging.sub_sock(sock, timeout=1000)
|
||||
zmq_sleep()
|
||||
|
||||
# no msg in queue, socket should timeout
|
||||
recvd = messaging.recv_one(sub_sock)
|
||||
@@ -142,12 +123,10 @@ class TestMessaging:
|
||||
assert isinstance(recvd, capnp._DynamicStructReader)
|
||||
assert_carstate(msg.carState, recvd.carState)
|
||||
|
||||
@pytest.mark.xfail(condition="ZMQ" in os.environ, reason='ZMQ detected')
|
||||
def test_recv_one_or_none(self):
|
||||
sock = "carState"
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sub_sock = messaging.sub_sock(sock)
|
||||
zmq_sleep()
|
||||
|
||||
# no msg in queue, socket shouldn't block
|
||||
recvd = messaging.recv_one_or_none(sub_sock)
|
||||
@@ -165,16 +144,13 @@ class TestMessaging:
|
||||
sock_timeout = 0.1
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000))
|
||||
zmq_sleep()
|
||||
|
||||
# this test doesn't work with ZMQ since multiprocessing interrupts it
|
||||
if "ZMQ" not in os.environ:
|
||||
# wait 5 socket timeouts and make sure it's still retrying
|
||||
p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,))
|
||||
p.start()
|
||||
time.sleep(sock_timeout*5)
|
||||
assert p.is_alive()
|
||||
p.terminate()
|
||||
# wait 5 socket timeouts and make sure it's still retrying
|
||||
p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,))
|
||||
p.start()
|
||||
time.sleep(sock_timeout*5)
|
||||
assert p.is_alive()
|
||||
p.terminate()
|
||||
|
||||
# wait 5 socket timeouts before sending
|
||||
msg = random_carstate()
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import random
|
||||
import time
|
||||
from typing import Sized, cast
|
||||
from typing import cast
|
||||
from collections.abc import Sized
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \
|
||||
random_bytes, random_carstate, assert_carstate, \
|
||||
zmq_sleep
|
||||
random_bytes, random_carstate, assert_carstate
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
|
||||
class TestSubMaster:
|
||||
|
||||
def setup_method(self):
|
||||
# ZMQ pub socket takes too long to die
|
||||
# sleep to prevent multiple publishers error between tests
|
||||
zmq_sleep(3)
|
||||
|
||||
def test_init(self):
|
||||
sm = messaging.SubMaster(events)
|
||||
for p in [sm.updated, sm.recv_time, sm.recv_frame, sm.alive,
|
||||
@@ -42,7 +37,6 @@ class TestSubMaster:
|
||||
sock = "carState"
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sm = messaging.SubMaster([sock,])
|
||||
zmq_sleep()
|
||||
|
||||
msg = random_carstate()
|
||||
pub_sock.send(msg.to_bytes())
|
||||
@@ -54,7 +48,6 @@ class TestSubMaster:
|
||||
sock = "carState"
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sm = messaging.SubMaster([sock,])
|
||||
zmq_sleep()
|
||||
|
||||
for i in range(10):
|
||||
msg = messaging.new_message(sock)
|
||||
@@ -91,21 +84,13 @@ class TestSubMaster:
|
||||
|
||||
for service, (max_freq, min_freq) in checks.items():
|
||||
if max_freq is not None:
|
||||
assert min_freq is not None
|
||||
assert sm._check_avg_freq(service)
|
||||
assert sm.freq_tracker[service].max_freq == max_freq*1.2
|
||||
assert sm.freq_tracker[service].min_freq == min_freq*0.8
|
||||
else:
|
||||
assert not sm._check_avg_freq(service)
|
||||
|
||||
def test_alive(self):
|
||||
pass
|
||||
|
||||
def test_ignore_alive(self):
|
||||
pass
|
||||
|
||||
def test_valid(self):
|
||||
pass
|
||||
|
||||
# SubMaster should always conflate
|
||||
def test_conflate(self):
|
||||
sock = "carState"
|
||||
@@ -124,11 +109,6 @@ class TestSubMaster:
|
||||
|
||||
class TestPubMaster:
|
||||
|
||||
def setup_method(self):
|
||||
# ZMQ pub socket takes too long to die
|
||||
# sleep to prevent multiple publishers error between tests
|
||||
zmq_sleep(3)
|
||||
|
||||
def test_init(self):
|
||||
messaging.PubMaster(events)
|
||||
|
||||
@@ -136,7 +116,6 @@ class TestPubMaster:
|
||||
socks = random_socks()
|
||||
pm = messaging.PubMaster(socks)
|
||||
sub_socks = {s: messaging.sub_sock(s, conflate=True, timeout=1000) for s in socks}
|
||||
zmq_sleep()
|
||||
|
||||
# PubMaster accepts either a capnp msg builder or bytes
|
||||
for capnp in [True, False]:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# TODO: this should be automatically determined using the capnp schema
|
||||
@@ -11,7 +10,7 @@ class QueueSize(IntEnum):
|
||||
|
||||
|
||||
class Service:
|
||||
def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None,
|
||||
def __init__(self, should_log: bool, frequency: float, decimation: int | None = None,
|
||||
queue_size: QueueSize = QueueSize.SMALL):
|
||||
self.should_log = should_log
|
||||
self.frequency = frequency
|
||||
@@ -125,8 +124,7 @@ def build_header():
|
||||
for k, v in SERVICE_LIST.items():
|
||||
should_log = "true" if v.should_log else "false"
|
||||
decimation = -1 if v.decimation is None else v.decimation
|
||||
h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \
|
||||
(k, k, should_log, v.frequency, decimation, v.queue_size)
|
||||
h += f' {{ "{k}", {{"{k}", {should_log}, {v.frequency:f}, {decimation:d}, {v.queue_size:d}}}}},\n'
|
||||
h += "};\n"
|
||||
|
||||
h += "#endif\n"
|
||||
|
||||
@@ -38,6 +38,7 @@ class BaseApi:
|
||||
}
|
||||
if payload_extra is not None:
|
||||
payload.update(payload_extra)
|
||||
assert self.private_key is not None
|
||||
token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
|
||||
if isinstance(token, bytes):
|
||||
token = token.decode('utf8')
|
||||
|
||||
@@ -42,25 +42,26 @@ def get_existing_chunks(path):
|
||||
class ChunkStream(io.RawIOBase):
|
||||
def __init__(self, paths):
|
||||
self._paths = iter(paths)
|
||||
self._buf = memoryview(b'')
|
||||
self._f = None
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def readinto(self, b):
|
||||
n = 0
|
||||
view = memoryview(b)
|
||||
while n < len(b):
|
||||
if not self._buf:
|
||||
if self._f is None:
|
||||
p = next(self._paths, None)
|
||||
if p is None:
|
||||
break
|
||||
with open(p, 'rb') as f:
|
||||
self._buf = memoryview(f.read())
|
||||
self._f = open(p, 'rb')
|
||||
count = self._f.readinto(view[n:])
|
||||
if not count:
|
||||
self._f.close()
|
||||
self._f = None
|
||||
continue
|
||||
take = min(len(b) - n, len(self._buf))
|
||||
b[n:n + take] = self._buf[:take]
|
||||
self._buf = self._buf[take:]
|
||||
n += take
|
||||
n += count
|
||||
return n
|
||||
|
||||
def open_file_chunked(path):
|
||||
|
||||
@@ -56,28 +56,28 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz",
|
||||
"hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
|
||||
"hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39.img.xz",
|
||||
"hash": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
|
||||
"hash_raw": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
|
||||
"size": 17487872,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8"
|
||||
"ondevice_hash": "ddfe93cc6a8531af92ee331d9bbaeae2f1d933bdb38e579769dc9fe7998eb626"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz",
|
||||
"hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113",
|
||||
"hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img.xz",
|
||||
"hash": "4dc41c2c072f5f5d5cd484cd6173049cd96acfb9a67bc20049775585fe881539",
|
||||
"hash_raw": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342",
|
||||
"ondevice_hash": "cf1229630b7a2b8497705bca4ba947dbf0c217418ff4febff571aa4f4a878134",
|
||||
"alt": {
|
||||
"hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img",
|
||||
"hash": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class StreamingDecompressor:
|
||||
def __init__(self, url: str) -> None:
|
||||
self.buf = b""
|
||||
|
||||
self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60)
|
||||
self.req = requests.get(url, stream=True, headers={'Accept-Encoding': 'identity'}, timeout=60)
|
||||
self.it = self.req.iter_content(chunk_size=1024 * 1024)
|
||||
self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO)
|
||||
self.eof = False
|
||||
|
||||
@@ -339,7 +339,7 @@ class Tici(HardwareBase):
|
||||
|
||||
# Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL
|
||||
gpio_init(GPIO.SOM_ST_IO, True)
|
||||
gpio_set(GPIO.SOM_ST_IO, 1)
|
||||
gpio_set(GPIO.SOM_ST_IO, True)
|
||||
|
||||
# *** IRQ config ***
|
||||
|
||||
@@ -389,21 +389,21 @@ class Tici(HardwareBase):
|
||||
gpio_init(GPIO.STM_RST_N, True)
|
||||
gpio_init(GPIO.STM_BOOT0, True)
|
||||
|
||||
gpio_set(GPIO.STM_RST_N, 1)
|
||||
gpio_set(GPIO.STM_BOOT0, 0)
|
||||
gpio_set(GPIO.STM_RST_N, True)
|
||||
gpio_set(GPIO.STM_BOOT0, False)
|
||||
time.sleep(0.01)
|
||||
gpio_set(GPIO.STM_RST_N, 0)
|
||||
gpio_set(GPIO.STM_RST_N, False)
|
||||
|
||||
def recover_internal_panda(self):
|
||||
gpio_init(GPIO.STM_RST_N, True)
|
||||
gpio_init(GPIO.STM_BOOT0, True)
|
||||
|
||||
gpio_set(GPIO.STM_RST_N, 1)
|
||||
gpio_set(GPIO.STM_BOOT0, 1)
|
||||
gpio_set(GPIO.STM_RST_N, True)
|
||||
gpio_set(GPIO.STM_BOOT0, True)
|
||||
time.sleep(0.01)
|
||||
gpio_set(GPIO.STM_RST_N, 0)
|
||||
gpio_set(GPIO.STM_RST_N, False)
|
||||
time.sleep(0.01)
|
||||
gpio_set(GPIO.STM_BOOT0, 0)
|
||||
gpio_set(GPIO.STM_BOOT0, False)
|
||||
|
||||
def booted(self):
|
||||
# this normally boots within 8s, but on rare occasions takes 30+s
|
||||
|
||||
@@ -52,7 +52,7 @@ PPPD_CMD = [
|
||||
"novj", "novjccomp", "ipcp-accept-local", "ipcp-accept-remote", "nomagic",
|
||||
"user", '""', "password", '""',
|
||||
]
|
||||
INITIAL_STATE = {
|
||||
INITIAL_STATE: dict[str, object] = {
|
||||
"seconds_since_boot": 0,
|
||||
"state": "INITIALIZING",
|
||||
"connected": False, "ip_address": "",
|
||||
|
||||
@@ -37,6 +37,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}},
|
||||
{"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}},
|
||||
{"ExperimentalMode", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"ExperimentalModeConfirmed", {PERSISTENT | BACKUP, BOOL}},
|
||||
@@ -221,6 +222,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import numpy as np
|
||||
from numbers import Number
|
||||
from collections.abc import Sequence
|
||||
|
||||
Gain = int | float | tuple[Sequence[float], Sequence[float]] | list[list[float]]
|
||||
|
||||
class PIDController:
|
||||
def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
|
||||
self._k_p: list[list[float]] = [[0], [k_p]] if isinstance(k_p, Number) else k_p
|
||||
self._k_i: list[list[float]] = [[0], [k_i]] if isinstance(k_i, Number) else k_i
|
||||
self._k_d: list[list[float]] = [[0], [k_d]] if isinstance(k_d, Number) else k_d
|
||||
def __init__(self, k_p: Gain, k_i: Gain, k_d: Gain = 0., pos_limit=1e308, neg_limit=-1e308, rate=100):
|
||||
self._k_p = ([0], [k_p]) if isinstance(k_p, (int, float)) else k_p
|
||||
self._k_i = ([0], [k_i]) if isinstance(k_i, (int, float)) else k_i
|
||||
self._k_d = ([0], [k_d]) if isinstance(k_d, (int, float)) else k_d
|
||||
|
||||
self.set_limits(pos_limit, neg_limit)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler):
|
||||
self.log_files = self.get_existing_logfiles()
|
||||
log_indexes = [f.split(".")[-1] for f in self.log_files]
|
||||
self.last_file_idx = max([int(i) for i in log_indexes if i.isdigit()] or [-1])
|
||||
self.last_rollover = None
|
||||
self.last_rollover = 0.0
|
||||
self.doRollover()
|
||||
|
||||
def _open(self):
|
||||
@@ -39,7 +39,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler):
|
||||
return stream
|
||||
|
||||
def get_existing_logfiles(self):
|
||||
log_files = list()
|
||||
log_files = []
|
||||
base_dir = os.path.dirname(self.base_filename)
|
||||
for fn in os.listdir(base_dir):
|
||||
fp = os.path.join(base_dir, fn)
|
||||
|
||||
@@ -112,14 +112,14 @@ class TestParams:
|
||||
def test_params_default_value(self):
|
||||
self.params.remove("LanguageSetting")
|
||||
self.params.remove("LongitudinalPersonality")
|
||||
self.params.remove("LiveParameters")
|
||||
self.params.remove("LiveParametersV2")
|
||||
|
||||
assert self.params.get("LanguageSetting") is None
|
||||
assert self.params.get("LanguageSetting", return_default=False) is None
|
||||
assert isinstance(self.params.get("LanguageSetting", return_default=True), str)
|
||||
assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int)
|
||||
assert self.params.get("LiveParameters") is None
|
||||
assert self.params.get("LiveParameters", return_default=True) is None
|
||||
assert self.params.get("LiveParametersV2") is None
|
||||
assert self.params.get("LiveParametersV2", return_default=True) is None
|
||||
|
||||
def test_params_get_type(self):
|
||||
# json
|
||||
|
||||
@@ -52,7 +52,7 @@ _ar_ox_config = DeviceCameraConfig(CameraConfig(1928, 1208, 2648.0), _ar_ox_fish
|
||||
_os_config = DeviceCameraConfig(CameraConfig(2688 // 2, 1520 // 2, 1522.0 * 3 / 4), _os_fisheye, _os_fisheye)
|
||||
_neo_config = DeviceCameraConfig(CameraConfig(1164, 874, 910.0), CameraConfig(816, 612, 650.0), _NoneCameraConfig())
|
||||
|
||||
DEVICE_CAMERAS = {
|
||||
DEVICE_CAMERAS: dict[tuple[str, str], DeviceCameraConfig] = {
|
||||
# A "device camera" is defined by a device type and sensor
|
||||
|
||||
# sensor type was never set on eon/neo/two
|
||||
@@ -176,4 +176,3 @@ def img_from_device(pt_device):
|
||||
|
||||
pt_img = pt_view/pt_view[:, 2:3]
|
||||
return pt_img.reshape(input_shape)[:, :2]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3027da9834adf9c71177bc3086f8de3018ce22e4725b694d635723c5b3c860cd
|
||||
size 97120
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca29ff46fbd6d00cc02596de8c9abfbf1cacdc6a7b2f98b27a74d24c790da004
|
||||
size 52374
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:42bd04a57b527c787a0555503e02a203f7d672c12d448769a3f41f17befbf013
|
||||
size 48044
|
||||
oid sha256:6f47633b5082b911e79dd13fc2d1f4c2a9d44d2fe0860f3d390ffc4e3d772f7d
|
||||
size 97120
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:538271820d618046009bb456e930598f2736dd1a6174c11925ec3912581b05c1
|
||||
size 52374
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:634fad6590295ef2e73d5f7e5aecbb5dd245a0f7692f1e300c2ebac95aabb8e3
|
||||
size 73026
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b1e177499d9439367179cc57a6301b6162393972e3a136cc35c5fdac026bf10a
|
||||
size 48044
|
||||
oid sha256:a380fe0a022856b302841a0dd19b71eab318c6b08ec851f128942758a7f1631a
|
||||
size 97120
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2c0af7f5fe57bb36ab96fae868e20feca763541c97a61b3a3a84a0e7fcb81163
|
||||
size 83350
|
||||
oid sha256:cc9e67cfaba77e8e4f4049f0add9238660e4cf8f886c6915600d8f47a30c98db
|
||||
size 97120
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ad19268e4aaaeac8dd21f6b26c16a121e7b3f50bba867748e7226727643ae682
|
||||
size 144642
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:412ef25d2fb103c1ebd55c667313a5921493305fb4e1f4e1dafc08d3b95d86ab
|
||||
size 73026
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4af81cbf1d96a42cc351878b015298aee82874b46baaf1a615ca91ec36c0ced6
|
||||
size 83228
|
||||
oid sha256:081d64ca28a84a59ff5e6aea4732865b8687585938b8a287bc7e6a49d0646508
|
||||
size 97120
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1bb440f424c989c06e203bfba0b32d39aede8f6c78a9b335e5e778460202b601
|
||||
size 73026
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5a390831afca3bfc6ea3c2739b872ebf866e70df8ae30653f8587e5cd3993959
|
||||
size 68306
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67e636d072703e6b1233a12344c0a6304fd43d64dbb31c66b71c2c8870a339c1
|
||||
size 153764
|
||||
@@ -56,6 +56,9 @@ class CarSpecificEvents:
|
||||
if self.CP.minEnableSpeed > 0 and CS.vEgo < 0.001:
|
||||
events.add(EventName.manualRestart)
|
||||
|
||||
if CS.brakeHoldActive and CS.blockPcmEnable: # set by Nidec Hybrid which cannot resume from brakehold
|
||||
events.add(EventName.belowEngageSpeed)
|
||||
|
||||
elif self.CP.brand == 'toyota':
|
||||
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
|
||||
@@ -3,7 +3,7 @@ import copy
|
||||
import os
|
||||
import pytest
|
||||
import random
|
||||
import unittest # noqa: TID251
|
||||
import unittest
|
||||
from collections import defaultdict, Counter
|
||||
import hypothesis.strategies as st
|
||||
from hypothesis import Phase, given, settings
|
||||
@@ -22,6 +22,7 @@ from openpilot.selfdrive.pandad import can_capnp_to_list
|
||||
from openpilot.selfdrive.test.helpers import read_segment_list
|
||||
from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
|
||||
from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source
|
||||
from openpilot.tools.lib.file_sources import Source
|
||||
from openpilot.tools.lib.route import SegmentName
|
||||
|
||||
SafetyModel = car.CarParams.SafetyModel
|
||||
@@ -131,7 +132,7 @@ class TestCarModelBase(unittest.TestCase):
|
||||
segment_range = f"{cls.test_route.route}/{seg}"
|
||||
|
||||
try:
|
||||
sources = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source]
|
||||
sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source]
|
||||
lr = LogReader(segment_range, sources=sources, sort_by_time=True)
|
||||
return cls.get_testing_data_from_logreader(lr)
|
||||
except (LogsUnavailable, AssertionError):
|
||||
@@ -257,7 +258,7 @@ class TestCarModelBase(unittest.TestCase):
|
||||
|
||||
# Don't check relay malfunction on disabled routes (relay closed),
|
||||
# or before fingerprinting is done (elm327 and noOutput)
|
||||
if self.openpilot_enabled and t / 1e4 > self.car_safety_mode_frame:
|
||||
if self.car_safety_mode_frame is not None and t / 1e4 > self.car_safety_mode_frame:
|
||||
self.assertFalse(self.safety.get_relay_malfunction())
|
||||
else:
|
||||
self.safety.set_relay_malfunction(False)
|
||||
@@ -451,7 +452,7 @@ class TestCarModelBase(unittest.TestCase):
|
||||
# TODO: remove this exception once this mismatch is resolved
|
||||
brake_pressed = CS.brakePressed
|
||||
if CS.brakePressed and not self.safety.get_brake_pressed_prev():
|
||||
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brakeDEPRECATED > 0.05:
|
||||
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.deprecated.brake > 0.05:
|
||||
brake_pressed = False
|
||||
checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev()
|
||||
checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev()
|
||||
|
||||
@@ -15,6 +15,9 @@ MAX_LATERAL_JERK = 5.0 # m/s^3
|
||||
MAX_LATERAL_ACCEL_NO_ROLL = 3.0 # m/s^2
|
||||
|
||||
|
||||
def should_stop(v_ego: float, a_target: float) -> bool:
|
||||
return bool(v_ego < 0.25 and a_target < 0.1)
|
||||
|
||||
def clamp(val, min_val, max_val):
|
||||
clamped_val = float(np.clip(val, min_val, max_val))
|
||||
return clamped_val, clamped_val != val
|
||||
@@ -40,7 +43,7 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, b
|
||||
return float(new_curvature), limited_accel or limited_max_curv
|
||||
|
||||
|
||||
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.3):
|
||||
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL):
|
||||
if len(speeds) == len(t_idxs):
|
||||
v_now = speeds[0]
|
||||
a_now = accels[0]
|
||||
@@ -53,8 +56,7 @@ def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.
|
||||
v_now = 0.0
|
||||
v_target = 0.0
|
||||
a_target = 0.0
|
||||
should_stop = (v_now < vEgoStopping and a_target < 0.1)
|
||||
return a_target, should_stop
|
||||
return a_target, should_stop(v_now, a_target)
|
||||
|
||||
def curv_from_psis(psi_target, psi_rate, vego, action_t):
|
||||
vego = np.clip(vego, MIN_SPEED, np.inf)
|
||||
|
||||
@@ -14,7 +14,7 @@ class LatControl(ABC):
|
||||
self.steer_max = 1.0
|
||||
|
||||
@abstractmethod
|
||||
def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose,
|
||||
def update(self, active: bool, CS, VM, params, steer_limited_by_safety: bool, desired_curvature: float, calibrated_pose: Pose | None,
|
||||
curvature_limited: bool, lat_delay: float):
|
||||
pass
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
|
||||
LongCtrlState = car.CarControl.Actuators.LongControlState
|
||||
|
||||
|
||||
def long_control_state_trans(CP, CP_SP, active, long_control_state, v_ego,
|
||||
def long_control_state_trans(CP_SP, active, long_control_state,
|
||||
should_stop, brake_pressed, cruise_standstill):
|
||||
# Gas Interceptor
|
||||
cruise_standstill = cruise_standstill and not CP_SP.enableGasInterceptor
|
||||
@@ -26,22 +26,17 @@ def long_control_state_trans(CP, CP_SP, active, long_control_state, v_ego,
|
||||
if long_control_state == LongCtrlState.off:
|
||||
if not starting_condition:
|
||||
long_control_state = LongCtrlState.stopping
|
||||
elif CP.startingState:
|
||||
long_control_state = LongCtrlState.starting
|
||||
else:
|
||||
long_control_state = LongCtrlState.pid
|
||||
|
||||
elif long_control_state == LongCtrlState.stopping:
|
||||
if starting_condition and CP.startingState:
|
||||
long_control_state = LongCtrlState.starting
|
||||
elif starting_condition:
|
||||
if starting_condition:
|
||||
long_control_state = LongCtrlState.pid
|
||||
|
||||
elif long_control_state in [LongCtrlState.starting, LongCtrlState.pid]:
|
||||
elif long_control_state == LongCtrlState.pid:
|
||||
if should_stop:
|
||||
long_control_state = LongCtrlState.stopping
|
||||
elif v_ego > CP.vEgoStarting:
|
||||
long_control_state = LongCtrlState.pid
|
||||
|
||||
return long_control_state
|
||||
|
||||
class LongControl:
|
||||
@@ -62,7 +57,7 @@ class LongControl:
|
||||
self.pid.neg_limit = accel_limits[0]
|
||||
self.pid.pos_limit = accel_limits[1]
|
||||
|
||||
self.long_control_state = long_control_state_trans(self.CP, self.CP_SP, active, self.long_control_state, CS.vEgo,
|
||||
self.long_control_state = long_control_state_trans(self.CP_SP, active, self.long_control_state,
|
||||
should_stop, CS.brakePressed,
|
||||
CS.cruiseState.standstill)
|
||||
if self.long_control_state == LongCtrlState.off:
|
||||
@@ -73,11 +68,8 @@ class LongControl:
|
||||
output_accel = self.last_output_accel
|
||||
if output_accel > self.CP.stopAccel:
|
||||
output_accel = min(output_accel, 0.0)
|
||||
output_accel -= self.CP.stoppingDecelRate * DT_CTRL
|
||||
self.reset()
|
||||
|
||||
elif self.long_control_state == LongCtrlState.starting:
|
||||
output_accel = self.CP.startAccel
|
||||
# TODO: can we just go straight to stopAccel?
|
||||
output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop
|
||||
self.reset()
|
||||
|
||||
else: # LongCtrlState.pid
|
||||
|
||||
@@ -137,7 +137,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
|
||||
action_t = self.CP.longitudinalActuatorDelay + DT_MDL
|
||||
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
|
||||
action_t=action_t, vEgoStopping=self.CP.vEgoStopping)
|
||||
action_t=action_t)
|
||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ def main():
|
||||
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
|
||||
'liveMapDataSP', 'carStateSP', gps_location_service],
|
||||
poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
|
||||
'liveMapDataSP', 'carStateSP', 'selfdriveStateSP', gps_location_service],
|
||||
poll='modelV2', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
longitudinal_planner.sla.update_car_state(sm['carState'])
|
||||
longitudinal_planner.sla.update_buttons(sm['selfdriveStateSP'].buttonsReleaseToggle)
|
||||
if sm.updated['modelV2']:
|
||||
longitudinal_planner.update(sm)
|
||||
longitudinal_planner.publish(sm, pm)
|
||||
|
||||
@@ -1,60 +1,43 @@
|
||||
from openpilot.cereal import custom
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
|
||||
|
||||
|
||||
|
||||
|
||||
class TestLongControlStateTransition:
|
||||
|
||||
def test_stay_stopped(self):
|
||||
CP = car.CarParams.new_message()
|
||||
CP_SP = custom.CarParamsSP.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=True, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=True, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=True)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
active = False
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.off
|
||||
|
||||
def test_engage():
|
||||
CP = car.CarParams.new_message()
|
||||
CP_SP = custom.CarParamsSP.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.off
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=True, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=True, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=True)
|
||||
assert next_state == LongCtrlState.stopping
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
def test_starting():
|
||||
CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5)
|
||||
CP_SP = custom.CarParamsSP.new_message()
|
||||
active = True
|
||||
current_state = LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.starting
|
||||
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
@@ -7,8 +7,6 @@ from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS
|
||||
|
||||
np.random.seed(0)
|
||||
|
||||
LA_ERR_STD = 1.0
|
||||
INPUT_NOISE_STD = 0.08
|
||||
V_EGO = 30.0
|
||||
@@ -58,16 +56,17 @@ def simulate_straight_road_msgs(est):
|
||||
for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)):
|
||||
est.handle_log(t, which, msg)
|
||||
|
||||
def test_estimated_offset():
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
msg = est.get_msg()
|
||||
# TODO add lataccelfactor and friction check when we have more accurate estimates
|
||||
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
|
||||
class TestTorquedLatAccelOffset:
|
||||
def test_estimated_offset(self):
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
msg = est.get_msg()
|
||||
# TODO add lataccelfactor and friction check when we have more accurate estimates
|
||||
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
|
||||
|
||||
def test_straight_road_roll_bias():
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
simulate_straight_road_msgs(est)
|
||||
msg = est.get_msg()
|
||||
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)
|
||||
def test_straight_road_roll_bias(self):
|
||||
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
|
||||
est = get_warmed_up_estimator(steer_torques, lat_accels)
|
||||
simulate_straight_road_msgs(est)
|
||||
msg = est.get_msg()
|
||||
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import numpy as np
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from functools import cache
|
||||
|
||||
@@ -68,7 +69,8 @@ class NPQueue:
|
||||
|
||||
|
||||
class PointBuckets:
|
||||
def __init__(self, x_bounds: list[tuple[float, float]], min_points: list[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None:
|
||||
def __init__(self, x_bounds: list[tuple[float, float]], min_points: Sequence[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None:
|
||||
self._rng = np.random.default_rng()
|
||||
self.x_bounds = x_bounds
|
||||
self.buckets = {bounds: NPQueue(maxlen=points_per_bucket, rowsize=rowsize) for bounds in x_bounds}
|
||||
self.buckets_min_points = dict(zip(x_bounds, min_points, strict=True))
|
||||
@@ -98,9 +100,9 @@ class PointBuckets:
|
||||
points = np.vstack([x.arr for x in self.buckets.values()])
|
||||
if num_points is None:
|
||||
return points
|
||||
return points[np.random.choice(np.arange(len(points)), min(len(points), num_points), replace=False)]
|
||||
return points[self._rng.choice(np.arange(len(points)), min(len(points), num_points), replace=False)]
|
||||
|
||||
def load_points(self, points: list[list[float]]) -> None:
|
||||
def load_points(self, points: Sequence[Sequence[float]]) -> None:
|
||||
for point in points:
|
||||
self.add_point(*point)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class LocationEstimator:
|
||||
self.observations = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds}
|
||||
self.observation_errors = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds}
|
||||
|
||||
def reset(self, t: float, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P):
|
||||
def reset(self, t: float | None, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P):
|
||||
self.kf.init_state(x_initial, covs=P_initial, filter_time=t)
|
||||
|
||||
def _validate_sensor_source(self, source: log.SensorEventData.SensorSource):
|
||||
|
||||
@@ -200,25 +200,6 @@ def check_valid_with_hysteresis(current_valid: bool, val: float, threshold: floa
|
||||
return current_valid
|
||||
|
||||
|
||||
# TODO: Remove this function after few releases (added in 0.9.9)
|
||||
def migrate_cached_vehicle_params_if_needed(params: Params):
|
||||
last_parameters_data_old = params.get("LiveParameters")
|
||||
last_parameters_data = params.get("LiveParametersV2")
|
||||
if last_parameters_data_old is None or last_parameters_data is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
last_parameters_msg = messaging.new_message('liveParameters')
|
||||
last_parameters_msg.liveParameters.valid = True
|
||||
last_parameters_msg.liveParameters.steerRatio = last_parameters_data_old['steerRatio']
|
||||
last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_data_old['stiffnessFactor']
|
||||
last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_data_old['angleOffsetAverageDeg']
|
||||
params.put("LiveParametersV2", last_parameters_msg.to_bytes(), block=True)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to perform parameter migration: {e}")
|
||||
params.remove("LiveParameters")
|
||||
|
||||
|
||||
def retrieve_initial_vehicle_params(params: Params, CP: car.CarParams, replay: bool, debug: bool):
|
||||
last_parameters_data = params.get("LiveParametersV2")
|
||||
last_carparams_data = params.get("CarParamsPrevRoute")
|
||||
@@ -273,8 +254,6 @@ def main():
|
||||
params = Params()
|
||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||
|
||||
migrate_cached_vehicle_params_if_needed(params)
|
||||
|
||||
steer_ratio, stiffness_factor, angle_offset_deg, pInitial = retrieve_initial_vehicle_params(params, CP, REPLAY, DEBUG)
|
||||
learner = VehicleParamsLearner(CP, steer_ratio, stiffness_factor, np.radians(angle_offset_deg), pInitial)
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ class TestLagd:
|
||||
assert retrieve_initial_lag(params, CP) is None
|
||||
|
||||
def test_ncc(self):
|
||||
rng = np.random.default_rng()
|
||||
lag_frames = random.randint(1, 19)
|
||||
|
||||
desired_sig = np.sin(np.arange(0.0, 10.0, 0.1))
|
||||
@@ -91,15 +92,15 @@ class TestLagd:
|
||||
assert np.argmax(corr) == lag_frames
|
||||
|
||||
# add some noise
|
||||
desired_sig += np.random.normal(0, 0.05, len(desired_sig))
|
||||
actual_sig += np.random.normal(0, 0.05, len(actual_sig))
|
||||
desired_sig += rng.normal(0, 0.05, len(desired_sig))
|
||||
actual_sig += rng.normal(0, 0.05, len(actual_sig))
|
||||
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
|
||||
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
|
||||
|
||||
# mask out 40% of the values, and make them noise
|
||||
mask = np.random.choice([True, False], size=len(desired_sig), p=[0.6, 0.4])
|
||||
desired_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask))
|
||||
actual_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask))
|
||||
mask = rng.choice([True, False], size=len(desired_sig), p=[0.6, 0.4])
|
||||
desired_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask))
|
||||
actual_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask))
|
||||
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
|
||||
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ def get_select_fields_data(logs):
|
||||
def sig_smooth(signal):
|
||||
return masked_symmetric_moving_average(signal, np.ones_like(signal), 5, 1.0)
|
||||
def get_nested_keys(msg, keys):
|
||||
val = None
|
||||
val = msg
|
||||
for key in keys:
|
||||
val = getattr(msg if val is None else val, key) if isinstance(key, str) else val[key]
|
||||
val = getattr(val, key) if isinstance(key, str) else val[key]
|
||||
return val
|
||||
lp = [x.livePose for x in logs if x.which() == 'livePose']
|
||||
data = defaultdict(list)
|
||||
|
||||
@@ -2,7 +2,7 @@ import random
|
||||
import numpy as np
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params, migrate_cached_vehicle_params_if_needed
|
||||
from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params
|
||||
from openpilot.selfdrive.locationd.models.car_kf import CarKalman
|
||||
from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE
|
||||
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
|
||||
@@ -30,38 +30,9 @@ class TestParamsd:
|
||||
params.put("LiveParametersV2", msg.to_bytes(), block=True)
|
||||
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True)
|
||||
|
||||
migrate_cached_vehicle_params_if_needed(params) # this is not tested here but should not mess anything up or throw an error
|
||||
sr, sf, offset, p_init = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True)
|
||||
np.testing.assert_allclose(sr, msg.liveParameters.steerRatio)
|
||||
np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor)
|
||||
np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg)
|
||||
np.testing.assert_equal(p_init.shape, CarKalman.P_initial.shape)
|
||||
np.testing.assert_allclose(np.diagonal(p_init), msg.liveParameters.debugFilterState.std)
|
||||
|
||||
# TODO Remove this test after the support for old format is removed
|
||||
def test_read_saved_params_old_format(self):
|
||||
params = Params()
|
||||
|
||||
lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams])
|
||||
CP = next(m for m in lr if m.which() == "carParams").carParams
|
||||
|
||||
msg = get_random_live_parameters(CP)
|
||||
params.put("LiveParameters", msg.liveParameters.to_dict(), block=True)
|
||||
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True)
|
||||
params.remove("LiveParametersV2")
|
||||
|
||||
migrate_cached_vehicle_params_if_needed(params)
|
||||
sr, sf, offset, _ = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True)
|
||||
np.testing.assert_allclose(sr, msg.liveParameters.steerRatio)
|
||||
np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor)
|
||||
np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg)
|
||||
assert params.get("LiveParametersV2") is not None
|
||||
|
||||
def test_read_saved_params_corrupted_old_format(self):
|
||||
params = Params()
|
||||
params.put("LiveParameters", {}, block=True)
|
||||
params.remove("LiveParametersV2")
|
||||
|
||||
migrate_cached_vehicle_params_if_needed(params)
|
||||
assert params.get("LiveParameters") is None
|
||||
assert params.get("LiveParametersV2") is None
|
||||
|
||||
@@ -2,24 +2,25 @@ from opendbc.car.structs import car
|
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
|
||||
|
||||
|
||||
def test_cal_percent():
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 0
|
||||
class TestTorqued:
|
||||
def test_cal_percent(self):
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 0
|
||||
|
||||
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
|
||||
est.filtered_points.buckets_min_points.values(), strict=True):
|
||||
for _ in range(int(min_pts)):
|
||||
est.filtered_points.add_point((low + high) / 2.0, 0.0)
|
||||
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
|
||||
est.filtered_points.buckets_min_points.values(), strict=True):
|
||||
for _ in range(int(min_pts)):
|
||||
est.filtered_points.add_point((low + high) / 2.0, 0.0)
|
||||
|
||||
# enough bucket points, but not enough total points
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
|
||||
# enough bucket points, but not enough total points
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
|
||||
|
||||
# add enough points to bucket with most capacity
|
||||
key = list(est.filtered_points.buckets)[0]
|
||||
for _ in range(est.min_points_total - len(est.filtered_points)):
|
||||
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
|
||||
# add enough points to bucket with most capacity
|
||||
key = list(est.filtered_points.buckets)[0]
|
||||
for _ in range(est.min_points_total - len(est.filtered_points)):
|
||||
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
|
||||
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 100
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 100
|
||||
|
||||
@@ -62,14 +62,14 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
|
||||
self.lag = 0.0
|
||||
self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters
|
||||
if decimated:
|
||||
self.min_bucket_points = MIN_BUCKET_POINTS / 10
|
||||
self.min_bucket_points: list[float] = (MIN_BUCKET_POINTS / 10).tolist()
|
||||
self.min_points_total = MIN_POINTS_TOTAL_QLOG
|
||||
self.fit_points = FIT_POINTS_TOTAL_QLOG
|
||||
self.factor_sanity = FACTOR_SANITY_QLOG
|
||||
self.friction_sanity = FRICTION_SANITY_QLOG
|
||||
|
||||
else:
|
||||
self.min_bucket_points = MIN_BUCKET_POINTS
|
||||
self.min_bucket_points = MIN_BUCKET_POINTS.tolist()
|
||||
self.min_points_total = MIN_POINTS_TOTAL
|
||||
self.fit_points = FIT_POINTS_TOTAL
|
||||
self.factor_sanity = FACTOR_SANITY
|
||||
@@ -120,9 +120,10 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
|
||||
'latAccelOffset': cache_ltp.latAccelOffsetFiltered,
|
||||
'frictionCoefficient': cache_ltp.frictionCoefficientFiltered
|
||||
}
|
||||
initial_params['points'] = cache_ltp.points
|
||||
cached_points: list[list[float]] = [list(point) for point in cache_ltp.points]
|
||||
initial_params['points'] = cached_points
|
||||
self.decay = cache_ltp.decay
|
||||
self.filtered_points.load_points(initial_params['points'])
|
||||
self.filtered_points.load_points(cached_points)
|
||||
cloudlog.info("restored torque params from cache")
|
||||
except Exception:
|
||||
cloudlog.exception("failed to restore cached torque params")
|
||||
|
||||
@@ -13,6 +13,7 @@ from collections import namedtuple
|
||||
import numpy as np
|
||||
|
||||
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob
|
||||
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
||||
|
||||
def _patch_tinygrad_fetch_fw():
|
||||
import hashlib
|
||||
@@ -242,7 +243,7 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||
SEED = 42
|
||||
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||
input_queues, npy = make_queues(Device.DEFAULT)
|
||||
np.random.seed(seed)
|
||||
rng = np.random.default_rng(seed)
|
||||
Tensor.manual_seed(seed)
|
||||
|
||||
testing = test_val is not None or test_buffers is not None
|
||||
@@ -250,7 +251,7 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||
|
||||
for i in range(n_runs):
|
||||
for v in npy.values():
|
||||
v[:] = np.random.randn(*v.shape).astype(v.dtype)
|
||||
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||
Device.default.synchronize()
|
||||
random_inputs = make_random_inputs()
|
||||
st = time.perf_counter()
|
||||
@@ -311,6 +312,9 @@ if __name__ == "__main__":
|
||||
p.add_argument('--frame-skip', type=int, required=True)
|
||||
args = p.parse_args()
|
||||
|
||||
if 'USB+AMD' in os.environ.get('DEV', ''):
|
||||
wait_usbgpu_link()
|
||||
|
||||
model_path = read_file_chunked_to_disk(args.onnx)
|
||||
model_w, model_h = args.model_size
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_drivi
|
||||
from openpilot.common.file_chunker import open_file_chunked, get_manifest_path
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
@@ -94,7 +95,7 @@ class ModelState(ModelStateBase):
|
||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||
self.full_frames: dict[str, Tensor] = {}
|
||||
self._blob_cache: dict[int, Tensor] = {}
|
||||
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||
self.parser = Parser()
|
||||
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||
self.run_policy = jits['run_policy']
|
||||
@@ -173,6 +174,8 @@ def main(demo=False):
|
||||
if use_extra_client:
|
||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||
|
||||
if USBGPU:
|
||||
wait_usbgpu_link()
|
||||
st = time.monotonic()
|
||||
cloudlog.warning("loading model")
|
||||
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU)
|
||||
|
||||
@@ -41,7 +41,7 @@ class Parser:
|
||||
raw = outs[name]
|
||||
outs[name] = sigmoid(raw)
|
||||
|
||||
def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None):
|
||||
def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=()):
|
||||
if self.check_missing(outs, name):
|
||||
return
|
||||
raw = outs[name]
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.hardware.usb import CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID, usb_devices, controller, read_int
|
||||
|
||||
STABLE_SECONDS = 2.0
|
||||
STABLE_THRESHOLD = 5.0 # link errors per second
|
||||
|
||||
|
||||
def _chestnut_portli() -> Path | None:
|
||||
for device in usb_devices():
|
||||
if read_int(device / "idVendor", 16) == CHESTNUT_VENDOR_ID and \
|
||||
read_int(device / "idProduct", 16) == CHESTNUT_PRODUCT_ID:
|
||||
ctrl = controller(device)
|
||||
if ctrl is not None and (ctrl / "portli").exists():
|
||||
return ctrl / "portli"
|
||||
return None
|
||||
|
||||
|
||||
def wait_usbgpu_link(timeout: float = 30.0) -> None:
|
||||
portli = _chestnut_portli()
|
||||
if portli is None:
|
||||
return
|
||||
|
||||
t0 = time.monotonic()
|
||||
while time.monotonic() - t0 < timeout:
|
||||
start = read_int(portli, 0)
|
||||
time.sleep(STABLE_SECONDS)
|
||||
rate = (read_int(portli, 0) - start) / STABLE_SECONDS
|
||||
if rate <= STABLE_THRESHOLD:
|
||||
return
|
||||
cloudlog.warning(f"usbgpu link not stable: {rate:.0f} errors/s")
|
||||
cloudlog.error("usbgpu link never stabilized")
|
||||
@@ -41,7 +41,7 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
# lockout specs
|
||||
self._MAX_ALERT_3 = 2
|
||||
self._MAX_NO_RESPONSE = 1
|
||||
self._LOCKOUT_TIME = int(1800 / DT_DMON)
|
||||
self._LOCKOUT_TIMES = [int(60 * n_min / DT_DMON) for n_min in [1, 5, 15, 30]]
|
||||
|
||||
self._TIMEOUT_RECOVERY_FACTOR_MAX = 5.
|
||||
self._TIMEOUT_RECOVERY_FACTOR_MIN = 1.25
|
||||
@@ -152,7 +152,10 @@ class DriverMonitoring:
|
||||
self.cnt_since_alert_3 = 0
|
||||
self.no_response_timeout = int(self.settings._NO_RESPONSE_TIMEOUT / DT_DMON)
|
||||
self.no_response_cnt = 0
|
||||
self.lockout_time = 0
|
||||
self.lockout_active = Params().get_bool("DriverTooDistracted")
|
||||
self.lockout_count = Params().get("DriverLockoutCount") or 0
|
||||
self.lockout_duration = self.settings._LOCKOUT_TIMES[min(max(self.lockout_count - 1, 0), len(self.settings._LOCKOUT_TIMES) - 1)]
|
||||
self.lockout_time_elapsed = 0
|
||||
self.step_change = 0.
|
||||
self.active_policy = MonitoringPolicy.vision
|
||||
self.driver_interacting = False
|
||||
@@ -163,7 +166,6 @@ class DriverMonitoring:
|
||||
self.threshold_alert_2 = 0.
|
||||
self.dcam_uncertain_cnt = 0
|
||||
self.dcam_reset_cnt = 0
|
||||
self.too_distracted = Params().get_bool("DriverTooDistracted")
|
||||
|
||||
self._reset_awareness()
|
||||
self._set_policy(MonitoringPolicy.vision)
|
||||
@@ -310,16 +312,20 @@ class DriverMonitoring:
|
||||
self.driver_interacting = driver_engaged
|
||||
|
||||
if self.alert_3_cnt >= self.settings._MAX_ALERT_3 or self.no_response_cnt >= self.settings._MAX_NO_RESPONSE:
|
||||
self.too_distracted = True
|
||||
if not self.lockout_active:
|
||||
self.lockout_count += 1
|
||||
self.lockout_duration = self.settings._LOCKOUT_TIMES[min(self.lockout_count - 1, len(self.settings._LOCKOUT_TIMES) - 1)]
|
||||
Params().put("DriverLockoutCount", self.lockout_count)
|
||||
self.lockout_active = True
|
||||
|
||||
if self.too_distracted:
|
||||
self.lockout_time += 1
|
||||
if self.lockout_time > self.settings._LOCKOUT_TIME:
|
||||
self.too_distracted = False
|
||||
if self.lockout_active:
|
||||
self.lockout_time_elapsed += 1
|
||||
if self.lockout_time_elapsed > self.lockout_duration:
|
||||
self.lockout_active = False
|
||||
self.alert_3_cnt = 0
|
||||
self.cnt_since_alert_3 = 0
|
||||
self.no_response_cnt = 0
|
||||
self.lockout_time = 0
|
||||
self.lockout_time_elapsed = 0
|
||||
|
||||
always_on_valid = self.always_on and not wrong_gear
|
||||
if (self.driver_interacting and self.awareness > 0 and self.active_policy == MonitoringPolicy.wheeltouch) or \
|
||||
@@ -379,8 +385,10 @@ class DriverMonitoring:
|
||||
dat = messaging.new_message('driverMonitoringState', valid=valid)
|
||||
dm = dat.driverMonitoringState
|
||||
|
||||
dm.lockout = self.too_distracted
|
||||
dm.lockoutRecoveryPercent = to_percent(self.lockout_time / self.settings._LOCKOUT_TIME)
|
||||
dm.lockout = self.lockout_active
|
||||
dm.lockoutCount = self.lockout_count
|
||||
if self.lockout_active:
|
||||
dm.lockoutMinutesRemaining = max(1, round((self.lockout_duration - self.lockout_time_elapsed) * DT_DMON / 60.))
|
||||
dm.alert3Count = self.alert_3_cnt
|
||||
dm.noResponseCount = self.no_response_cnt
|
||||
dm.noResponseForceDecel = self.alert_level == AlertLevel.three and self.cnt_since_alert_3 >= self.no_response_timeout
|
||||
|
||||
@@ -86,21 +86,17 @@ class TestMonitoring:
|
||||
# engaged, distracted past red and beyond the no-response window -> unavailability response + lockout
|
||||
def test_distracted_lockout(self):
|
||||
alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
|
||||
s = d_status.settings
|
||||
assert alert_lvls[int(DISTRACTED_SECONDS_TO_RED / DT_DMON)] == 3
|
||||
assert d_status.alert_3_cnt == 1
|
||||
assert d_status.no_response_cnt == s._MAX_NO_RESPONSE
|
||||
assert d_status.too_distracted
|
||||
assert d_status.lockout_time > 0
|
||||
assert d_status.lockout_active
|
||||
assert d_status.lockout_time_elapsed > 0
|
||||
assert d_status.lockout_count >= 1
|
||||
|
||||
# no face -> wheeltouch red, sustained past the no-response timeout -> unavailability response + lockout
|
||||
def test_invisible_lockout(self):
|
||||
_, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
|
||||
s = d_status.settings
|
||||
assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.wheeltouch
|
||||
assert d_status.alert_3_cnt == 1
|
||||
assert d_status.no_response_cnt == s._MAX_NO_RESPONSE
|
||||
assert d_status.too_distracted
|
||||
assert d_status.lockout_active
|
||||
assert d_status.lockout_count >= 1
|
||||
|
||||
# engaged, no face detected the whole time, no action
|
||||
def test_fully_invisible_driver(self):
|
||||
@@ -275,7 +271,7 @@ def test_run_step_engagement(selfdrive_enabled, lat_active, steering, gas,
|
||||
captured['op_engaged'] = op_engaged
|
||||
return orig(driver_engaged, op_engaged, lowspeed, wrong_gear)
|
||||
|
||||
dm._update_events = spy
|
||||
object.__setattr__(dm, '_update_events', spy)
|
||||
dm.run_step(sm, demo=False)
|
||||
assert captured['op_engaged'] == expected_op_engaged
|
||||
assert captured['driver_engaged'] == expected_driver_engaged
|
||||
|
||||
@@ -60,7 +60,7 @@ class TestPandad:
|
||||
|
||||
def test_in_reset(self):
|
||||
gpio_init(GPIO.STM_RST_N, True)
|
||||
gpio_set(GPIO.STM_RST_N, 1)
|
||||
gpio_set(GPIO.STM_RST_N, True)
|
||||
assert not Panda.list()
|
||||
self._run_test()
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ class TestBoarddSpi:
|
||||
|
||||
total_recv_count = 0
|
||||
total_sent_count = 0
|
||||
sent_msgs = {bus: list() for bus in range(3)}
|
||||
sent_msgs = {bus: [] for bus in range(3)}
|
||||
|
||||
st = time.monotonic()
|
||||
ts = {s: list() for s in socks.keys()}
|
||||
ts = {s: [] for s in socks.keys()}
|
||||
for _ in range(int(os.getenv("TEST_TIME", "20"))):
|
||||
# send some CAN messages
|
||||
if not JUNGLE_SPAM:
|
||||
|
||||
@@ -7,9 +7,8 @@ from opendbc.car.structs import car
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.git import get_short_branch
|
||||
from openpilot.common.realtime import DT_CTRL, DT_DMON
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER
|
||||
from openpilot.selfdrive.monitoring.policy import DRIVER_MONITOR_SETTINGS
|
||||
from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER
|
||||
from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
@@ -25,7 +24,6 @@ VisualAlert = car.CarControl.HUDControl.VisualAlert
|
||||
AudibleAlert = log.SelfdriveState.AudibleAlert
|
||||
EventName = log.OnroadEvent.EventName
|
||||
|
||||
DMON_LOCKOUT_TIME = DRIVER_MONITOR_SETTINGS()._LOCKOUT_TIME
|
||||
|
||||
# get event name from enum
|
||||
EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()}
|
||||
@@ -101,7 +99,7 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
|
||||
|
||||
def too_distracted_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
||||
if sm['driverMonitoringState'].lockout:
|
||||
mins_left = max(1, round((100 - sm['driverMonitoringState'].lockoutRecoveryPercent) / 100 * DMON_LOCKOUT_TIME * DT_DMON / 60.))
|
||||
mins_left = sm['driverMonitoringState'].lockoutMinutesRemaining
|
||||
return NoEntryAlert("Too Distracted", f"{mins_left} minute{'s' if mins_left != 1 else ''} Left", priority=Priority.HIGH)
|
||||
return NoEntryAlert("Pay Attention to Engage", priority=Priority.HIGH)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from openpilot.sunnypilot import get_sanitize_int_param
|
||||
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
||||
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
||||
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
REPLAY = "REPLAY" in os.environ
|
||||
@@ -177,6 +178,7 @@ class SelfdriveD(CruiseHelper):
|
||||
self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP)
|
||||
|
||||
CruiseHelper.__init__(self, self.CP)
|
||||
self.button_state_tracker = ButtonStateTracker()
|
||||
|
||||
def update_events(self, CS):
|
||||
"""Compute onroadEvents from carState"""
|
||||
@@ -597,6 +599,8 @@ class SelfdriveD(CruiseHelper):
|
||||
icbm.sendButton = self.icbm.cruise_button
|
||||
icbm.vTarget = self.icbm.v_target
|
||||
|
||||
self.button_state_tracker.publish(ss_sp)
|
||||
|
||||
self.pm.send('selfdriveStateSP', ss_sp_msg)
|
||||
|
||||
# onroadEventsSP - logged every second or on change
|
||||
@@ -616,6 +620,7 @@ class SelfdriveD(CruiseHelper):
|
||||
self.mads.update(CS)
|
||||
self.update_alerts(CS)
|
||||
|
||||
self.button_state_tracker.update(CS)
|
||||
self.publish_selfdriveState(CS)
|
||||
|
||||
self.CS_prev = CS
|
||||
|
||||
@@ -13,11 +13,11 @@ ALL_STATES = tuple(State.schema.enumerants.values())
|
||||
ENABLE_EVENT_TYPES = (ET.ENABLE, ET.PRE_ENABLE, ET.OVERRIDE_LATERAL, ET.OVERRIDE_LONGITUDINAL)
|
||||
|
||||
|
||||
def make_event(event_types):
|
||||
event = {}
|
||||
def make_event(event_types: list[str | None]):
|
||||
EVENTS[0] = {}
|
||||
for ev in event_types:
|
||||
event[ev] = NormalPermanentAlert("alert")
|
||||
EVENTS[0] = event
|
||||
if ev is not None:
|
||||
EVENTS[0][ev] = NormalPermanentAlert("alert")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from openpilot.common.utils import tabulate
|
||||
|
||||
DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496"
|
||||
MB = 1024 * 1024
|
||||
TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center")
|
||||
TABULATE_OPTS = {"tablefmt": "simple_grid", "stralign": "center", "numalign": "center"}
|
||||
|
||||
|
||||
def _get_procs():
|
||||
|
||||
@@ -297,7 +297,7 @@ def migrate_carOutput(msgs):
|
||||
co = messaging.new_message('carOutput')
|
||||
co.valid = msg.valid
|
||||
co.logMonoTime = msg.logMonoTime
|
||||
co.carOutput.actuatorsOutput = msg.carControl.actuatorsOutputDEPRECATED
|
||||
co.carOutput.actuatorsOutput = msg.carControl.deprecated.actuatorsOutput
|
||||
add_ops.append(as_reader(co))
|
||||
return [], add_ops, []
|
||||
|
||||
@@ -323,10 +323,10 @@ def migrate_pandaStates(msgs):
|
||||
safety_param = safety_param_migration[fingerprint].value
|
||||
elif len(CP.safetyConfigs):
|
||||
safety_param = CP.safetyConfigs[0].safetyParam
|
||||
if CP.safetyConfigs[0].safetyParamDEPRECATED != 0:
|
||||
safety_param = CP.safetyConfigs[0].safetyParamDEPRECATED
|
||||
if CP.safetyConfigs[0].deprecated.safetyParam != 0:
|
||||
safety_param = CP.safetyConfigs[0].deprecated.safetyParam
|
||||
else:
|
||||
safety_param = CP.safetyParamDEPRECATED
|
||||
safety_param = CP.deprecated.safetyParam
|
||||
|
||||
ops = []
|
||||
for index, msg in msgs:
|
||||
|
||||
@@ -216,8 +216,7 @@ class ProcessContainer:
|
||||
|
||||
def _start_process(self):
|
||||
if self.capture is not None:
|
||||
self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher)
|
||||
self.process.prepare()
|
||||
self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher) # ty: ignore[invalid-assignment] # intentional wrapper
|
||||
self.process.start()
|
||||
|
||||
def start(
|
||||
@@ -635,10 +634,10 @@ def replay_process(
|
||||
fingerprint: str | None = None, return_all_logs: bool = False, custom_params: dict[str, Any] | None = None,
|
||||
captured_output_store: dict[str, dict[str, str]] | None = None, disable_progress: bool = False
|
||||
) -> list[capnp._DynamicStructReader]:
|
||||
if isinstance(cfg, Iterable):
|
||||
cfgs = list(cfg)
|
||||
else:
|
||||
if isinstance(cfg, ProcessConfig):
|
||||
cfgs = [cfg]
|
||||
else:
|
||||
cfgs = list(cfg)
|
||||
|
||||
all_msgs = migrate_all(lr,
|
||||
manager_states=True,
|
||||
|
||||
@@ -204,7 +204,10 @@ class FaceAnimator:
|
||||
frames_back = round(rewind_elapsed / self._animation.frame_duration)
|
||||
frame_index = self._rewind_from - frames_back
|
||||
if frame_index <= 0:
|
||||
return self._switch_to_next(now)
|
||||
if self._next is None:
|
||||
self._rewinding = False
|
||||
return self._animation.frames[0]
|
||||
return self._switch_to_next(now, self._next)
|
||||
return self._animation.frames[frame_index]
|
||||
|
||||
# Play starting frames first (once)
|
||||
@@ -223,7 +226,7 @@ class FaceAnimator:
|
||||
|
||||
if self._next is not None:
|
||||
if frame_index == 0 and (len(self._animation.frames) == 1 or self._seen_nonzero):
|
||||
return self._switch_to_next(now)
|
||||
return self._switch_to_next(now, self._next)
|
||||
# No natural return to frame 0 — start rewinding
|
||||
if self._animation.mode in (AnimationMode.ONCE_FORWARD, AnimationMode.REPEAT_FORWARD):
|
||||
self._rewinding = True
|
||||
@@ -232,8 +235,8 @@ class FaceAnimator:
|
||||
|
||||
return self._animation.frames[frame_index]
|
||||
|
||||
def _switch_to_next(self, now: float) -> list[tuple[int, int]]:
|
||||
self._animation = self._next
|
||||
def _switch_to_next(self, now: float, animation: Animation) -> list[tuple[int, int]]:
|
||||
self._animation = animation
|
||||
self._next = None
|
||||
self._rewinding = False
|
||||
self._seen_nonzero = False
|
||||
|
||||
@@ -13,6 +13,7 @@ from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.home import HomeLayoutSP as HomeLayout
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
@@ -34,7 +35,11 @@ class MainLayout(Widget):
|
||||
# Initialize layouts
|
||||
self._home_layout = HomeLayout()
|
||||
self._home_body_layout = BodyLayout()
|
||||
self._layouts = {MainState.HOME: self._home_layout, MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
||||
self._layouts: dict[MainState, Widget] = {
|
||||
MainState.HOME: self._home_layout,
|
||||
MainState.SETTINGS: SettingsLayout(),
|
||||
MainState.ONROAD: AugmentedRoadView(),
|
||||
}
|
||||
|
||||
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
@@ -105,7 +105,6 @@ class DeviceLayout(Widget):
|
||||
|
||||
self._params.remove("CalibrationParams")
|
||||
self._params.remove("LiveTorqueParameters")
|
||||
self._params.remove("LiveParameters")
|
||||
self._params.remove("LiveParametersV2")
|
||||
self._params.remove("LiveDelay")
|
||||
self._params.put_bool("OnroadCycleRequested", True, block=True)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
@@ -43,7 +43,7 @@ class PanelType(IntEnum):
|
||||
class PanelInfo:
|
||||
name: str
|
||||
instance: Widget
|
||||
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
button_rect: rl.Rectangle = field(default_factory=lambda: rl.Rectangle(0, 0, 0, 0))
|
||||
|
||||
|
||||
class SettingsLayout(Widget):
|
||||
|
||||
@@ -68,7 +68,7 @@ class Sidebar(Widget, SidebarSP):
|
||||
def __init__(self):
|
||||
Widget.__init__(self)
|
||||
SidebarSP.__init__(self)
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_type = NETWORK_TYPES[NetworkType.none]
|
||||
self._net_strength = 0
|
||||
|
||||
self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||
|
||||
@@ -251,7 +251,7 @@ class MiciHomeLayout(Widget):
|
||||
self._egpu_icon.set_visible(ui_state.usbgpu and ui_state.usbgpu_compiled)
|
||||
self._egpu_icon_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_compiled)
|
||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||
self._body_icon.set_visible(ui_state.is_body)
|
||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||
|
||||
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
|
||||
self._status_bar_layout.render(footer_rect)
|
||||
|
||||
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
@@ -148,4 +149,4 @@ class MiciMainLayout(Scroller):
|
||||
|
||||
def _on_body_changed(self):
|
||||
self._car_onroad_layout.set_visible(not ui_state.is_body)
|
||||
self._body_onroad_layout.set_visible(ui_state.is_body)
|
||||
self._body_onroad_layout.set_visible(bool(ui_state.is_body))
|
||||
|
||||
@@ -250,12 +250,12 @@ class MiciOffroadAlerts(Scroller):
|
||||
{alert_data.key: self.params.get(alert_data.key) for alert_data in self.sorted_alerts})
|
||||
time.sleep(REFRESH_INTERVAL)
|
||||
|
||||
def _refresh(self) -> int:
|
||||
def _refresh(self, pending_params: dict) -> int:
|
||||
"""Refresh alerts from params and return active count."""
|
||||
active_count = 0
|
||||
|
||||
# Handle UpdateAvailable alert specially
|
||||
update_available = self._pending_params["UpdateAvailable"]
|
||||
update_available = pending_params["UpdateAvailable"]
|
||||
update_alert_data = next((alert_data for alert_data in self.sorted_alerts if alert_data.key == "UpdateAvailable"), None)
|
||||
|
||||
if update_alert_data:
|
||||
@@ -263,7 +263,7 @@ class MiciOffroadAlerts(Scroller):
|
||||
version_string = ""
|
||||
|
||||
# Get new version description and parse version and date
|
||||
new_desc = self._pending_params["UpdaterNewDescription"] or ""
|
||||
new_desc = pending_params["UpdaterNewDescription"] or ""
|
||||
if new_desc:
|
||||
# format: "version / branch / commit / date"
|
||||
parts = new_desc.split(" / ")
|
||||
@@ -284,7 +284,7 @@ class MiciOffroadAlerts(Scroller):
|
||||
continue # Skip, already handled above
|
||||
|
||||
text = ""
|
||||
alert_json = self._pending_params[alert_data.key]
|
||||
alert_json = pending_params[alert_data.key]
|
||||
|
||||
if alert_json:
|
||||
text = alert_json.get("text", "").replace("%1", alert_json.get("extra", ""))
|
||||
@@ -311,8 +311,9 @@ class MiciOffroadAlerts(Scroller):
|
||||
def _update_state(self):
|
||||
"""Periodically refresh alerts."""
|
||||
# Refresh alerts when thread updates params
|
||||
if self._pending_params is not None:
|
||||
self._refresh()
|
||||
pending_params = self._pending_params
|
||||
if pending_params is not None:
|
||||
self._refresh(pending_params)
|
||||
self._pending_params = None
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
|
||||
@@ -16,7 +16,7 @@ from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
|
||||
from openpilot.system.ui.widgets.html_render import HtmlRenderer
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class DeviceLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._fcc_dialog: HtmlModal | None = None
|
||||
self._fcc_dialog: MiciFccModal | None = None
|
||||
|
||||
def power_off_callback():
|
||||
ui_state.params.put_bool("DoShutdown", True, block=True)
|
||||
@@ -172,7 +172,6 @@ class DeviceLayoutMici(NavScroller):
|
||||
params = ui_state.params
|
||||
params.remove("CalibrationParams")
|
||||
params.remove("LiveTorqueParameters")
|
||||
params.remove("LiveParameters")
|
||||
params.remove("LiveParametersV2")
|
||||
params.remove("LiveDelay")
|
||||
params.put_bool("OnroadCycleRequested", True, block=True)
|
||||
|
||||
@@ -219,7 +219,7 @@ class CameraView(Widget):
|
||||
[0.0, 0.0, 1.0]
|
||||
])
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
def _render(self, rect: rl.Rectangle, /):
|
||||
if self._switching:
|
||||
self._handle_switch()
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ class ModelPoints:
|
||||
|
||||
@dataclass
|
||||
class LeadVehicle:
|
||||
glow: list[float] = field(default_factory=list)
|
||||
chevron: list[float] = field(default_factory=list)
|
||||
glow: list[tuple[float, float]] = field(default_factory=list)
|
||||
chevron: list[tuple[float, float]] = field(default_factory=list)
|
||||
fill_alpha: int = 0
|
||||
|
||||
|
||||
|
||||
@@ -41,80 +41,81 @@ def get_child_widgets(widget) -> list:
|
||||
return children
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="segfaults")
|
||||
def test_dialogs_do_not_leak():
|
||||
import pyray as rl
|
||||
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
class TestWidgetLeaks:
|
||||
@pytest.mark.skip(reason="segfaults")
|
||||
def test_dialogs_do_not_leak(self):
|
||||
import pyray as rl
|
||||
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
# mici dialogs
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
|
||||
# mici dialogs
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
|
||||
|
||||
# tici dialogs
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
# tici dialogs
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
gui_app.init_window("ref-test")
|
||||
gui_app.init_window("ref-test")
|
||||
|
||||
leaked_widgets = set()
|
||||
leaked_widgets = set()
|
||||
|
||||
for ctor in (
|
||||
# mici
|
||||
MiciDriverCameraDialog, MiciPairingDialog,
|
||||
lambda: MiciTrainingGuide(lambda: None),
|
||||
lambda: MiciOnboardingWindow(lambda: None),
|
||||
lambda: BigDialog("test", "test"),
|
||||
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
|
||||
lambda: BigInputDialog("test"),
|
||||
lambda: MiciFccModal(text="test"),
|
||||
# tici
|
||||
TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard,
|
||||
lambda: ConfirmDialog("test", "ok"),
|
||||
lambda: MultiOptionDialog("test", ["a", "b"]),
|
||||
lambda: HtmlModal(text="test"),
|
||||
):
|
||||
widget = ctor()
|
||||
all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]]
|
||||
for ctor in (
|
||||
# mici
|
||||
MiciDriverCameraDialog, MiciPairingDialog,
|
||||
lambda: MiciTrainingGuide(lambda: None),
|
||||
lambda: MiciOnboardingWindow(lambda: None),
|
||||
lambda: BigDialog("test", "test"),
|
||||
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
|
||||
lambda: BigInputDialog("test"),
|
||||
lambda: MiciFccModal(text="test"),
|
||||
# tici
|
||||
TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard,
|
||||
lambda: ConfirmDialog("test", "ok"),
|
||||
lambda: MultiOptionDialog("test", ["a", "b"]),
|
||||
lambda: HtmlModal(text="test"),
|
||||
):
|
||||
widget = ctor()
|
||||
all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]]
|
||||
|
||||
del widget
|
||||
del widget
|
||||
|
||||
for ref in all_refs:
|
||||
if ref() is not None:
|
||||
obj = ref()
|
||||
name = f"{type(obj).__module__}.{type(obj).__qualname__}"
|
||||
leaked_widgets.add(name)
|
||||
for ref in all_refs:
|
||||
if ref() is not None:
|
||||
obj = ref()
|
||||
name = f"{type(obj).__module__}.{type(obj).__qualname__}"
|
||||
leaked_widgets.add(name)
|
||||
|
||||
print(f"\n=== Widget {name} alive after del")
|
||||
print(" Referrers:")
|
||||
for r in gc.get_referrers(obj):
|
||||
if r is obj:
|
||||
continue
|
||||
print(f"\n=== Widget {name} alive after del")
|
||||
print(" Referrers:")
|
||||
for r in gc.get_referrers(obj):
|
||||
if r is obj:
|
||||
continue
|
||||
|
||||
if hasattr(r, '__self__') and r.__self__ is not obj:
|
||||
print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}")
|
||||
elif hasattr(r, '__func__'):
|
||||
print(f" method: {r.__name__}")
|
||||
else:
|
||||
print(f" {type(r).__module__}.{type(r).__qualname__}")
|
||||
del obj
|
||||
if hasattr(r, '__self__') and r.__self__ is not obj:
|
||||
print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}")
|
||||
elif hasattr(r, '__func__'):
|
||||
print(f" method: {r.__name__}")
|
||||
else:
|
||||
print(f" {type(r).__module__}.{type(r).__qualname__}")
|
||||
del obj
|
||||
|
||||
gui_app.close()
|
||||
gui_app.close()
|
||||
|
||||
unexpected = leaked_widgets - KNOWN_LEAKS
|
||||
assert not unexpected, f"New leaked widgets: {unexpected}"
|
||||
unexpected = leaked_widgets - KNOWN_LEAKS
|
||||
assert not unexpected, f"New leaked widgets: {unexpected}"
|
||||
|
||||
fixed = KNOWN_LEAKS - leaked_widgets
|
||||
assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}"
|
||||
fixed = KNOWN_LEAKS - leaked_widgets
|
||||
assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_dialogs_do_not_leak()
|
||||
TestWidgetLeaks().test_dialogs_do_not_leak()
|
||||
|
||||
@@ -375,6 +375,7 @@ class GreyBigButton(BigButton):
|
||||
class BigMultiParamToggle(BigMultiToggle):
|
||||
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None):
|
||||
assert Params is not None
|
||||
super().__init__(text, options, toggle_callback, select_callback)
|
||||
self._param = param
|
||||
|
||||
@@ -392,6 +393,7 @@ class BigMultiParamToggle(BigMultiToggle):
|
||||
|
||||
class BigParamControl(BigToggle):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
||||
assert Params is not None
|
||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||
self.param = param
|
||||
self.params = Params()
|
||||
@@ -409,6 +411,7 @@ class BigParamControl(BigToggle):
|
||||
class BigCircleParamControl(BigCircleToggle):
|
||||
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None,
|
||||
icon_offset: tuple[int, int] = (0, 0)):
|
||||
assert Params is not None
|
||||
super().__init__(icon, toggle_callback, icon_offset=icon_offset)
|
||||
self._param = param
|
||||
self.params = Params()
|
||||
|
||||
@@ -39,8 +39,8 @@ class ModelPoints:
|
||||
|
||||
@dataclass
|
||||
class LeadVehicle:
|
||||
glow: list[float] = field(default_factory=list)
|
||||
chevron: list[float] = field(default_factory=list)
|
||||
glow: list[tuple[float, float]] = field(default_factory=list)
|
||||
chevron: list[tuple[float, float]] = field(default_factory=list)
|
||||
fill_alpha: int = 0
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate
|
||||
SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds
|
||||
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
|
||||
|
||||
AMBIENT_DB = 24 # DB where MIN_VOLUME is applied
|
||||
AMBIENT_DB = 26 # DB where MIN_VOLUME is applied
|
||||
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
|
||||
|
||||
VOLUME_BASE = 20
|
||||
@@ -48,14 +48,15 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
|
||||
AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME),
|
||||
AudibleAlert.refuse: ("refuse.wav", 1, MAX_VOLUME),
|
||||
|
||||
AudibleAlert.prompt: ("prompt.wav", 1, MAX_VOLUME),
|
||||
AudibleAlert.promptRepeat: ("prompt.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.promptDistracted: ("prompt_distracted.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.prompt: ("warning.wav", 1, MAX_VOLUME),
|
||||
AudibleAlert.promptRepeat: ("warning.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME),
|
||||
|
||||
AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME),
|
||||
AudibleAlert.complete: ("complete.wav", 1, MAX_VOLUME),
|
||||
|
||||
AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME),
|
||||
|
||||
**sound_list_sp,
|
||||
}
|
||||
@@ -89,6 +90,7 @@ class Soundd(QuietMode):
|
||||
self.ramp_start_time = 0.
|
||||
|
||||
self.selfdrive_timeout_alert = False
|
||||
self.pending_stop = False
|
||||
|
||||
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
|
||||
|
||||
@@ -127,6 +129,10 @@ class Soundd(QuietMode):
|
||||
self.current_sound_frame += frames_to_write
|
||||
current_sound_frame = self.current_sound_frame % len(sound_data)
|
||||
loops = self.current_sound_frame // len(sound_data)
|
||||
if self.pending_stop and current_sound_frame == 0:
|
||||
self.current_alert = AudibleAlert.none
|
||||
self.pending_stop = False
|
||||
break
|
||||
|
||||
return ret * self.current_volume
|
||||
|
||||
@@ -137,6 +143,15 @@ class Soundd(QuietMode):
|
||||
|
||||
def update_alert(self, new_alert):
|
||||
current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame >= len(self.loaded_sounds[self.current_alert])
|
||||
# let looping sounds finish the current loop instead of cutting off mid tone
|
||||
if new_alert == AudibleAlert.none and self.current_alert != AudibleAlert.none and sound_list[self.current_alert][1] is None:
|
||||
if current_alert_played_once:
|
||||
self.pending_stop = True
|
||||
else:
|
||||
self.current_alert = AudibleAlert.none
|
||||
self.current_sound_frame = 0
|
||||
return
|
||||
self.pending_stop = False
|
||||
if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once):
|
||||
if new_alert == AudibleAlert.warningImmediate:
|
||||
self.ramp_start_volume = self.current_volume
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
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 pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
BRAND_FONT_SIZE = 48
|
||||
BRAND_DESC_SPACING = 12
|
||||
|
||||
|
||||
class HomeLayoutSP(HomeLayout):
|
||||
def _render_header(self):
|
||||
font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
version_text_width = self.header_rect.width
|
||||
|
||||
if self.update_available:
|
||||
version_text_width -= self.update_notif_rect.width
|
||||
|
||||
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
|
||||
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
text = tr("UPDATE")
|
||||
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
|
||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
|
||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
|
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
if self.alert_count > 0:
|
||||
version_text_width -= self.alert_notif_rect.width
|
||||
|
||||
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
|
||||
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
|
||||
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
|
||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
|
||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
|
||||
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
if self.update_available or self.alert_count > 0:
|
||||
version_text_width -= SPACING * 1.5
|
||||
|
||||
version_right = self.header_rect.x + self.header_rect.width
|
||||
version_left = version_right - version_text_width
|
||||
|
||||
brand = "sunnypilot"
|
||||
description = self.params.get("UpdaterCurrentDescription") or ""
|
||||
|
||||
desc_width = 0
|
||||
if description:
|
||||
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
|
||||
desc_width = desc_size.x
|
||||
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
|
||||
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
|
||||
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
|
||||
spacing = BRAND_DESC_SPACING if description else 0
|
||||
brand_x = version_right - desc_width - spacing - brand_size.x
|
||||
brand_rect = rl.Rectangle(max(version_left, brand_x), self.header_rect.y, brand_size.x, self.header_rect.height)
|
||||
gui_label(brand_rect, brand, BRAND_FONT_SIZE, rl.WHITE, font_weight=FontWeight.AUDIOWIDE)
|
||||
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
|
||||
self._done_callback = done_callback
|
||||
self._step = 0
|
||||
|
||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||
|
||||
self._content = [
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ class ModelsLayout(Widget):
|
||||
self._initialize_items()
|
||||
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]:
|
||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay"), (self.camera_offset, "CameraOffset")]:
|
||||
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
||||
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
@@ -93,9 +93,14 @@ class ModelsLayout(Widget):
|
||||
|
||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||
|
||||
self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35,
|
||||
tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"),
|
||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
||||
lambda v: f"{v / 100:.2f} m")
|
||||
|
||||
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle,
|
||||
self.lane_turn_value_control, self.lagd_toggle, self.delay_control]
|
||||
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item,
|
||||
self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
|
||||
|
||||
def _update_lagd_description(self, lagd_toggle: bool):
|
||||
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
||||
@@ -232,14 +237,16 @@ class ModelsLayout(Widget):
|
||||
advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls")
|
||||
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
||||
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
||||
camera_offset: bool = ui_state.params.get("ModelManager_ActiveBundle") is not None
|
||||
|
||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||
self.lagd_toggle.action_item.set_state(live_delay)
|
||||
self.delay_control.set_visible(not live_delay and advanced_controls)
|
||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||
if self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||
self.lane_turn_value_control.action_item.value_change_step = new_step
|
||||
self.camera_offset.set_visible(camera_offset)
|
||||
|
||||
self._update_lagd_description(live_delay)
|
||||
self.model_manager = ui_state.sm["modelManagerSP"]
|
||||
|
||||
@@ -38,8 +38,8 @@ class NetworkUISP(NetworkUI):
|
||||
self.scan_button.set_text(tr("Scan"))
|
||||
self.scan_button.set_enabled(True)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
super()._render(rect)
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
|
||||
if self._current_panel == PanelType.WIFI:
|
||||
self.scan_button.set_position(self._rect.x, self._rect.y + 20)
|
||||
|
||||
@@ -37,7 +37,7 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
|
||||
ICON_SIZE = 70
|
||||
|
||||
OP.PanelType = IntEnum(
|
||||
OP.PanelType = IntEnum( # type: ignore[assignment] # ty: ignore[invalid-assignment]
|
||||
"PanelType",
|
||||
[es.name for es in OP.PanelType] + [
|
||||
"SUNNYLINK",
|
||||
@@ -180,20 +180,18 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
self._sidebar_scroller.render(nav_rect)
|
||||
return
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
|
||||
# Check close button
|
||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
return True
|
||||
return
|
||||
|
||||
# Check navigation buttons
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect) and self._sidebar_scroller.scroll_panel.is_touch_valid():
|
||||
self.set_current_panel(panel_type)
|
||||
return True
|
||||
|
||||
return False
|
||||
return
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
|
||||
+5
-2
@@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
from collections.abc import Callable
|
||||
import pyray as rl
|
||||
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
@@ -96,7 +96,10 @@ class MadsSettingsLayout(Widget):
|
||||
if brand == "rivian":
|
||||
return True
|
||||
elif brand == "tesla":
|
||||
return not (ui_state.CP_SP is not None and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
|
||||
if ui_state.CP_SP is None or not ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
|
||||
return True
|
||||
screen_button = int(ui_state.params.get("TeslaMadsScreenButton", return_default=True))
|
||||
return screen_button == MadsScreenButtonType.OFF
|
||||
return False
|
||||
|
||||
def _update_steering_mode_description(self, button_index: int):
|
||||
|
||||
@@ -4,10 +4,11 @@ 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 opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, toggle_item_sp
|
||||
|
||||
COOP_STEERING_MIN_KMH = 23
|
||||
OEM_STEERING_MIN_KMH = 48
|
||||
@@ -18,7 +19,14 @@ class TeslaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
|
||||
self.items = [self.coop_steering_toggle]
|
||||
self.mads_screen_button = multiple_button_item_sp(
|
||||
title=lambda: tr("MADS Screen Activation"),
|
||||
description="",
|
||||
buttons=[lambda: tr("Off"), lambda: tr("3-Finger"), lambda: tr("4-Finger"), lambda: tr("5-Finger")],
|
||||
param="TeslaMadsScreenButton",
|
||||
inline=False,
|
||||
)
|
||||
self.items = [self.coop_steering_toggle, self.mads_screen_button]
|
||||
|
||||
def update_settings(self):
|
||||
is_metric = ui_state.is_metric
|
||||
@@ -41,3 +49,18 @@ class TeslaSettings(BrandSettings):
|
||||
|
||||
self.coop_steering_toggle.set_description(coop_steering_desc)
|
||||
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
|
||||
has_vehicle_bus = ui_state.CP_SP is not None and bool(ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
|
||||
self.mads_screen_button.set_visible(has_vehicle_bus)
|
||||
|
||||
mads_screen_button_desc = (
|
||||
f"{tr('Use a multi-finger press on the infotainment screen to toggle MADS.')} " +
|
||||
f"{tr('This allows the use of full MADS functionality when enabled.')}<br><br>" +
|
||||
f"{tr('Selecting a higher finger count may reduce accidental activations.')}<br><br>" +
|
||||
f"<b>{tr('Note: Setting this to Off will reset your MADS settings to default.')}</b>"
|
||||
)
|
||||
if not ui_state.is_offroad():
|
||||
mads_screen_button_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to change.")
|
||||
mads_screen_button_desc = f"<b>{mads_screen_button_disabled_msg}</b><br><br>{mads_screen_button_desc}"
|
||||
self.mads_screen_button.set_description(mads_screen_button_desc)
|
||||
self.mads_screen_button.action_item.set_enabled(ui_state.is_offroad())
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
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 openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
|
||||
class MiciHomeLayoutSP(MiciHomeLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
||||
@@ -4,7 +4,6 @@ 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 collections.abc import Callable
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.cereal import custom
|
||||
@@ -48,10 +47,8 @@ class CurrentModelInfo(Widget):
|
||||
self.info_text.render()
|
||||
|
||||
class ModelsLayoutMici(NavScroller):
|
||||
def __init__(self, back_callback: Callable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
self.original_back_callback = back_callback
|
||||
self.focused_widget = None
|
||||
|
||||
self.current_model_info = CurrentModelInfo()
|
||||
@@ -85,12 +82,10 @@ class ModelsLayoutMici(NavScroller):
|
||||
|
||||
return folders
|
||||
|
||||
def _show_selection_view(self, items, back_callback: Callable):
|
||||
self._scroller._items = items
|
||||
for item in items:
|
||||
item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self.set_back_callback(back_callback)
|
||||
def _push_selection_view(self, items):
|
||||
scroller = NavScroller()
|
||||
scroller._scroller.add_widgets(items)
|
||||
gui_app.push_widget(scroller)
|
||||
|
||||
def _show_folders(self):
|
||||
self.focused_widget = self.select_model_btn
|
||||
@@ -112,15 +107,18 @@ class ModelsLayoutMici(NavScroller):
|
||||
folder_buttons.insert(0, btn)
|
||||
else:
|
||||
folder_buttons.append(btn)
|
||||
self._show_selection_view(folder_buttons, self._reset_main_view)
|
||||
self._push_selection_view(folder_buttons)
|
||||
|
||||
def _pop_to_main(self):
|
||||
gui_app.pop_widgets_to(self)
|
||||
|
||||
def _select_model(self, bundle):
|
||||
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
|
||||
self._reset_main_view()
|
||||
self._pop_to_main()
|
||||
|
||||
def _select_default(self):
|
||||
ui_state.params.remove("ModelManager_ActiveBundle")
|
||||
self._reset_main_view()
|
||||
self._pop_to_main()
|
||||
|
||||
def _select_folder(self, folder_name):
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
@@ -135,13 +133,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
btn = BigButton(txt)
|
||||
btn.set_click_callback(lambda b=bundle: self._select_model(b))
|
||||
btns.append(btn)
|
||||
self._show_selection_view(btns, self._show_folders)
|
||||
|
||||
def _reset_main_view(self):
|
||||
self._scroller._items = self.main_items
|
||||
self.set_back_callback(self.original_back_callback)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(0)
|
||||
self._push_selection_view(btns)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
|
||||
@@ -16,6 +16,9 @@ class SunnylinkConsentPage(NavScroller):
|
||||
def __init__(self, on_accept: Callable | None = None, on_decline: Callable | None = None):
|
||||
super().__init__()
|
||||
|
||||
assert on_accept is not None and callable(on_accept)
|
||||
assert on_decline is not None and callable(on_decline)
|
||||
|
||||
self._accept_button = BigConfirmationCircleButton("enable\nsunnylink", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64),
|
||||
on_accept, exit_on_confirm=False)
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
|
||||
|
||||
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
||||
sunnylink_panel = SunnylinkLayoutMici()
|
||||
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
|
||||
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
||||
|
||||
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
|
||||
models_panel = ModelsLayoutMici()
|
||||
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
|
||||
@@ -54,9 +53,8 @@ class SunnylinkInfo(Widget):
|
||||
self.sponsor_text.render()
|
||||
|
||||
class SunnylinkLayoutMici(NavScroller):
|
||||
def __init__(self, back_callback: Callable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
self._restore_in_progress = False
|
||||
self._backup_in_progress = False
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
@@ -198,7 +198,7 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self._draw_ahead_info(sign_rect)
|
||||
|
||||
def _draw_sign_main(self, rect, alpha=1.0):
|
||||
speed_limit_warning_enabled = ui_state.speed_limit_mode >= SpeedLimitMode.warning
|
||||
speed_limit_warning_enabled = ui_state.speed_limit_mode is not None and ui_state.speed_limit_mode >= SpeedLimitMode.warning
|
||||
has_limit = self.speed_limit_valid or self.speed_limit_last_valid
|
||||
is_overspeed = has_limit and round(self.speed_limit_final_last) < round(self.speed)
|
||||
|
||||
|
||||
@@ -224,8 +224,7 @@ class UIStateSP:
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
@staticmethod
|
||||
def _set_awake(on: bool, _ui_state):
|
||||
def _set_awake(self, on: bool, _ui_state=None):
|
||||
if _ui_state.boot_offroad_mode == 1 and not on:
|
||||
_ui_state.params.put_bool("OffroadMode", True)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user