mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-10-24 08:53:42 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d98fe4376 | |||
| 0ce9fca933 | |||
| 2110ffab00 | |||
| cda505ea6d | |||
| aa559e48a2 | |||
| 02c7b60600 | |||
| d28e027943 | |||
| f179460982 | |||
| 16b17e051f | |||
| 8efe325a71 | |||
| 9c391b4842 | |||
| 6ac88a7be0 | |||
| 4dbb49430b | |||
| 5de324ab96 | |||
| d904487eed | |||
| 2ffb986f3b | |||
| c4f1dc179f | |||
| 8e740e0ac6 | |||
| 15bf3dcd94 | |||
| 6e0ce4a6f9 | |||
| d45d7e2de7 | |||
| a3ef498512 | |||
| 21f45673fd | |||
| fd39f4f820 | |||
| cb71cdc54d | |||
| afd0ce66c9 | |||
| 5854346c2f | |||
| 270704ad47 | |||
| 9d029d683d | |||
| 4ebb756ee5 | |||
| f78c6c2377 | |||
| ee7bf203fe | |||
| 1aa3a430fb | |||
| ddd7b14ebc | |||
| 62cf10ff05 | |||
| 7707a3e384 | |||
| e87a605fb2 | |||
| 11603b2de7 | |||
| 644e23f8ef | |||
| 7847dbf148 | |||
| 0af369cae0 | |||
| 77d0c2d665 | |||
| 4d2d3f69f5 | |||
| e9fc7a2465 | |||
| 61154655cd | |||
| ed4e8aeea6 | |||
| a3ed6bd2f0 | |||
| 46c0b31997 | |||
| 8a50860648 | |||
| 563fa3c782 | |||
| 0b3459fbdc | |||
| 54a22bd8bd | |||
| 6c674810ee | |||
| e64f67ec7e | |||
| 9c7655c45e | |||
| 60c6464a14 | |||
| 922513f312 | |||
| 0dacd0eec4 | |||
| 09b2f4bbe0 | |||
| 0120b66f05 | |||
| f09b04c267 | |||
| f0617e11a6 | |||
| 0739fe823d | |||
| b6170ac5e9 | |||
| e6864661c9 | |||
| 4ff90ff8d0 | |||
| 2149006ec7 |
@@ -115,7 +115,7 @@ jobs:
|
||||
env ${TG_FLAGS} python3 ${{ github.workspace }}/${{ env.COMPILER }}/compile_onnx.py \
|
||||
${{ github.workspace }}/${{ env.SMALL_ONNX }} \
|
||||
${{ github.workspace }}/${{ env.SMALL_PKL }} \
|
||||
--device-input "*" --out-of-band --benchmark-runs 1
|
||||
--out-of-band --benchmark-runs 1
|
||||
|
||||
- name: Prepare output
|
||||
env:
|
||||
@@ -440,9 +440,10 @@ jobs:
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
|
||||
taskset -c 7 env ${TG_FLAGS} python3 \
|
||||
${{ github.workspace }}/tinygrad_repo/examples/openpilot/compile3.py \
|
||||
${{ github.workspace }}/tinygrad_repo/examples/openpilot/compile_onnx.py \
|
||||
${{ github.workspace }}/${{ env.DM_ONNX }} \
|
||||
${{ github.workspace }}/${{ env.DM_PKL }}
|
||||
${{ github.workspace }}/${{ env.DM_PKL }} \
|
||||
--out-of-band --benchmark-runs 1
|
||||
|
||||
- name: Compile DM warp
|
||||
run: |
|
||||
|
||||
@@ -58,6 +58,11 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: credential.https://huggingface.co.helper
|
||||
GIT_CONFIG_VALUE_0: '!f() { test "$1" = get && echo username=user && echo "password=$HF_TOKEN"; }; f'
|
||||
with:
|
||||
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -69,3 +74,7 @@ jobs:
|
||||
labels: bot
|
||||
add-paths: |
|
||||
openpilot/sunnypilot/modeld_v2/models/*.pkl
|
||||
|
||||
- name: Reset lfs access
|
||||
if: always()
|
||||
run: git config --local --unset-all "lfs.$(git config -f .lfsconfig lfs.url).access" || true
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- name: LFS Fetch
|
||||
run: |
|
||||
git lfs fetch
|
||||
git lfs fetch || echo "::warning::some objects are missing on commaai/openpilot-lfs; mirroring the rest"
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
@@ -68,6 +68,7 @@ jobs:
|
||||
git config lfs.activitytimeout 300
|
||||
git config lfs.transfer.maxretries 5
|
||||
git config lfs.concurrenttransfers 4
|
||||
git config lfs.allowincompletepush true
|
||||
|
||||
- name: Push LFS
|
||||
id: sync-and-commit
|
||||
@@ -81,3 +82,80 @@ jobs:
|
||||
done
|
||||
echo "All push attempts failed"
|
||||
exit 1
|
||||
|
||||
- name: Checkout sunnypilot
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'sunnypilot/sunnypilot'
|
||||
path: sunnypilot
|
||||
fetch-depth: 0
|
||||
filter: blob:limit=1k
|
||||
lfs: false
|
||||
|
||||
- name: Reference objects on sunnypilot-lfs
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
GIT_LFS_SKIP_SMUDGE=1 git clone --quiet https://huggingface.co/sunnypilot/sunnypilot-lfs hf-lfs
|
||||
mkdir -p hf-lfs/all
|
||||
cat > scan_pointers.py <<'EOF'
|
||||
import re, subprocess, sys, threading
|
||||
repo = sys.argv[1]
|
||||
chk = subprocess.run(["git", "-C", repo, "cat-file", "--batch-all-objects",
|
||||
"--batch-check=%(objectname) %(objecttype) %(objectsize)"],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
small = [f[0] for f in (l.split() for l in chk.splitlines()) if f[1] == "blob" and int(f[2]) < 1024]
|
||||
p = subprocess.Popen(["git", "-C", repo, "cat-file", "--batch=%(objectsize)"],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
def feed():
|
||||
for sha in small:
|
||||
p.stdin.write(sha.encode() + b"\n")
|
||||
p.stdin.close()
|
||||
threading.Thread(target=feed, daemon=True).start()
|
||||
rx = re.compile(rb"^version https://git-lfs\.github\.com/spec/v1\noid sha256:([0-9a-f]{64})\nsize ([0-9]+)\n")
|
||||
seen = {}
|
||||
for _ in small:
|
||||
n = int(p.stdout.readline()); body = p.stdout.read(n); p.stdout.read(1)
|
||||
m = rx.match(body)
|
||||
if m:
|
||||
seen.setdefault(m.group(1).decode(), m.group(2).decode())
|
||||
for oid, size in sorted(seen.items()):
|
||||
print(oid, size)
|
||||
EOF
|
||||
for repo in . sunnypilot; do
|
||||
python3 scan_pointers.py "$repo"
|
||||
done | sort -u | while read -r oid size; do
|
||||
[ -e "hf-lfs/all/$oid" ] && continue
|
||||
printf 'version https://git-lfs.github.com/spec/v1\noid sha256:%s\nsize %s\n' "$oid" "$size" > "hf-lfs/all/$oid"
|
||||
echo "$oid"
|
||||
done > new_oids.txt
|
||||
echo "new pointer files: $(wc -l < new_oids.txt)"
|
||||
|
||||
dropped=0
|
||||
while mapfile -t -n 100 chunk && ((${#chunk[@]})); do
|
||||
body=$(for oid in "${chunk[@]}"; do
|
||||
size=$(awk '/^size /{print $2}' "hf-lfs/all/$oid")
|
||||
printf '{"oid":"%s","size":%s}\n' "$oid" "$size"
|
||||
done | jq -s '{operation:"download",transfers:["basic"],objects:.}')
|
||||
for attempt in 1 2 3 4 5; do
|
||||
resp=$(curl -sS -u "user:${HF_TOKEN}" -w '\n%{http_code}' \
|
||||
-H 'Accept: application/vnd.git-lfs+json' -H 'Content-Type: application/vnd.git-lfs+json' \
|
||||
--data "$body" "${LFS_URL}/objects/batch") && [ "$(tail -n1 <<<"$resp")" = 200 ] && break
|
||||
echo "batch check attempt $attempt failed ($(tail -n1 <<<"$resp")), retrying in 60s"; sleep 60
|
||||
done
|
||||
[ "$(tail -n1 <<<"$resp")" = 200 ] || { echo "batch check failed"; exit 1; }
|
||||
for oid in $(sed '$d' <<<"$resp" | jq -r '.objects[] | select(.error) | .oid'); do
|
||||
rm -f "hf-lfs/all/$oid"; dropped=$((dropped+1))
|
||||
done
|
||||
done < new_oids.txt
|
||||
echo "dropped (not on HF): $dropped"
|
||||
|
||||
cd hf-lfs
|
||||
git add -A all
|
||||
if git diff --cached --quiet; then
|
||||
echo "nothing new to reference"
|
||||
exit 0
|
||||
fi
|
||||
git commit -q -m "Reference $(git diff --cached --name-only | wc -l | tr -d ' ') new LFS objects"
|
||||
git push origin HEAD:main
|
||||
|
||||
@@ -594,8 +594,14 @@ jobs:
|
||||
- name: Publish chestnut branch
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut"
|
||||
cp .lfsconfig "${{ env.OUTPUT_DIR }}/"
|
||||
echo 'openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text' > "${{ env.OUTPUT_DIR }}/.gitattributes"
|
||||
git lfs install
|
||||
git config --global credential.helper store
|
||||
echo "https://user:${HF_TOKEN}@huggingface.co" > ~/.git-credentials
|
||||
|
||||
${{ env.CI_DIR }}/publish.sh \
|
||||
"${{ github.workspace }}" \
|
||||
|
||||
@@ -3,9 +3,62 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'tinygrad_repo'
|
||||
- 'openpilot/sunnypilot/modeld_v2/**'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
|
||||
jobs:
|
||||
plain-artifact:
|
||||
name: Plain artifact round trip and pickle converter
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}
|
||||
TINYGRAD_TESTS: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build openpilot
|
||||
run: scons -j$(nproc)
|
||||
- name: Compile, load, cache and fuse a model with the pinned tinygrad
|
||||
env:
|
||||
DEV: CPU
|
||||
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_plain_artifact.py
|
||||
- name: Install LLVM 20 and ROCm comgr for the amd mock, as tinygrad_repo/.github/actions/setup-tinygrad does
|
||||
run: |
|
||||
. /etc/os-release
|
||||
curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
|
||||
echo "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/7.1 $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/rocm.list
|
||||
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600
|
||||
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends libllvm20 clang-20 lld-20 comgr
|
||||
printf '/opt/rocm/lib\n/opt/rocm/lib64\n' | sudo tee /etc/ld.so.conf.d/rocm.conf && sudo ldconfig
|
||||
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
|
||||
- name: Convert a pickled USB+AMD jit and replay it under tinygrad's amd mock
|
||||
env:
|
||||
DEV: MOCKUSB+AMD
|
||||
CHECK_OOB: 0
|
||||
GMMU: 0
|
||||
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_convert_upstream_pkl.py
|
||||
- name: Where tinygrad moved, for the helper or failure point named above
|
||||
if: failure() && github.event_name == 'pull_request'
|
||||
run: |
|
||||
git fetch -q --depth=1 origin "${{ github.base_ref }}"
|
||||
old=$(git rev-parse "origin/${{ github.base_ref }}:tinygrad_repo") new=$(git rev-parse HEAD:tinygrad_repo)
|
||||
git -C tinygrad_repo fetch -q origin "$old" || true
|
||||
echo "tinygrad_repo $old -> $new, in what helpers.py and plain_artifact_compiler.py read:"
|
||||
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/uop/ops.py tinygrad/engine/jit.py tinygrad/engine/realize.py tinygrad/device.py \
|
||||
tinygrad/helpers.py tinygrad/dtype.py tinygrad/renderer/__init__.py
|
||||
echo "and in what convert_upstream_pkl.py decodes:"
|
||||
git -C tinygrad_repo diff --stat "$old" "$new" -- tinygrad/runtime/autogen/am/pm4_nv.py tinygrad/runtime/autogen/amd_gpu.py
|
||||
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/runtime/support/hcq2.py tinygrad/runtime/support/usb.py tinygrad/runtime/ops_amd.py
|
||||
|
||||
generate-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 0041535cfb...e3f1487de4
@@ -138,6 +138,9 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
status @0 :DownloadStatus;
|
||||
progress @1 :Float32;
|
||||
eta @2 :UInt32;
|
||||
speed @3 :Float32;
|
||||
canceling @4 :Bool;
|
||||
resuming @5 :Bool;
|
||||
}
|
||||
|
||||
struct Chunk {
|
||||
@@ -465,6 +468,13 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
laneTurnDirection @0 :TurnDirection;
|
||||
leftLaneChangeEdgeBlock @1 :Bool;
|
||||
rightLaneChangeEdgeBlock @2 :Bool;
|
||||
blendfulCurvature @3 :Float32;
|
||||
blendfulTrust @4 :Float32;
|
||||
modelCurvature @5 :Float32;
|
||||
blendfulGoingAround @6 :Bool;
|
||||
blendfulLeftProb @7 :Float32;
|
||||
blendfulRightProb @8 :Float32;
|
||||
blendfulLaneWidth @9 :Float32;
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
|
||||
@@ -94,7 +94,7 @@ _services: dict[str, tuple] = {
|
||||
"carControlSP": (True, 100., 10),
|
||||
"carStateSP": (True, 100., 10),
|
||||
"liveMapDataSP": (True, 1., 1),
|
||||
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
||||
"modelDataV2SP": (True, 20., 10, QueueSize.BIG),
|
||||
"liveLocationKalman": (True, 20.),
|
||||
|
||||
# debug
|
||||
|
||||
@@ -256,7 +256,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
||||
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||
{"LanefulStrength", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||
{"Blendful", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
|
||||
|
||||
// mapd
|
||||
|
||||
@@ -48,8 +48,9 @@ def compile_model(onnx_path, pkl_path):
|
||||
Action(cmd, " [ONNX] $TARGET"),
|
||||
)
|
||||
|
||||
compile_model('models/dmonitoring_model.onnx', 'models/dmonitoring_model_tinygrad.pkl')
|
||||
compile_model('models/driving_supercombo.onnx', 'models/driving_tinygrad.pkl')
|
||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
compile_model('models/dmonitoring_model.onnx', 'models/dmonitoring_model_tinygrad.pkl')
|
||||
compile_model('models/driving_supercombo.onnx', 'models/driving_tinygrad.pkl')
|
||||
|
||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||
for cam_w, cam_h in camera_configs:
|
||||
@@ -62,7 +63,7 @@ for cam_w, cam_h in camera_configs:
|
||||
lenv.Command(warp_pkl_path, tinygrad_files + warp_deps + [Value(cmd)], cmd)
|
||||
|
||||
dm_w, dm_h = DM_INPUT_SIZE
|
||||
for cam_w, cam_h in camera_configs:
|
||||
for cam_w, cam_h in [] if os.getenv('SKIP_TINYGRAD_COMPILE') else camera_configs:
|
||||
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||
stride, y_height, uv_height, frame_size = get_nv12_info(cam_w, cam_h)
|
||||
cmd = (f'{tg_flags} {mac_brew_string} python3 "{compiler}/compile_warp.py" '
|
||||
|
||||
@@ -7,6 +7,7 @@ from functools import cached_property
|
||||
import os
|
||||
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
||||
os.environ.setdefault('AM_POWER_LIMIT', '100')
|
||||
os.environ.setdefault('AM_PERF_LEVEL', 'high')
|
||||
from tinygrad.device import Buffer, Device
|
||||
from tinygrad.dtype import DType, dtypes
|
||||
from tinygrad.engine.realize import lower_and_compile
|
||||
|
||||
@@ -206,8 +206,6 @@ class AugmentedRoadView(CameraView):
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
# Don't trigger click callback if bookmark was triggered
|
||||
if not self._bookmark_icon.interacting():
|
||||
if self._model_renderer.laneful.handle_tap(mouse_pos):
|
||||
return
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _render(self, _):
|
||||
|
||||
@@ -141,7 +141,7 @@ class ModelRenderer(Widget, ModelRendererSP):
|
||||
if ui_state.status != UIStatus.DISENGAGED:
|
||||
self._draw_lane_lines()
|
||||
self._draw_path()
|
||||
self.laneful.draw(self._rect)
|
||||
self.blendful.draw(self._rect)
|
||||
|
||||
if render_lead_indicator:
|
||||
self._update_leads(sm)
|
||||
@@ -220,7 +220,7 @@ class ModelRenderer(Widget, ModelRendererSP):
|
||||
# Update road edges using raw points
|
||||
for road_edge in self._road_edges:
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx)
|
||||
self.laneful.update(self, max_idx)
|
||||
self.blendful.update(self, max_idx)
|
||||
|
||||
# Update path using raw points
|
||||
if lead and lead.present:
|
||||
|
||||
@@ -123,6 +123,8 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
# Draw elements
|
||||
self._draw_lane_lines()
|
||||
self._draw_path()
|
||||
if self._lateral_active:
|
||||
self.blendful.draw(self._rect)
|
||||
|
||||
if render_lead_indicator and radar_state:
|
||||
self._draw_lead_indicator()
|
||||
@@ -172,6 +174,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
# Update road edges using raw points
|
||||
for road_edge in self._road_edges:
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance)
|
||||
self.blendful.update(self, max_idx, max_distance)
|
||||
|
||||
# Update path using raw points
|
||||
if lead and lead.present:
|
||||
|
||||
@@ -8,7 +8,6 @@ from enum import IntEnum
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_settings import SpeedLimitSettingsLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.mapd import MAPD_AVAILABLE
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, simple_button_item_sp
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -30,7 +29,6 @@ ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments f
|
||||
ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot longitudinal control enabled.")
|
||||
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
|
||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||
MAP_UNAVAILABLE_DESCRIPTION = tr_noop("Map data is temporarily unavailable.")
|
||||
|
||||
|
||||
class CruiseLayout(Widget):
|
||||
@@ -56,8 +54,7 @@ class CruiseLayout(Widget):
|
||||
|
||||
self.scc_m_toggle = toggle_item_sp(
|
||||
title=tr("Smart Cruise Control - Map"),
|
||||
description=(f"<b>{tr(MAP_UNAVAILABLE_DESCRIPTION)}</b>\n\n" if not MAPD_AVAILABLE else "") +
|
||||
tr("Use map data to estimate the appropriate speed to drive through turns ahead."),
|
||||
description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."),
|
||||
param="SmartCruiseControlMap")
|
||||
|
||||
self.custom_acc_toggle = toggle_item_sp(
|
||||
@@ -142,7 +139,7 @@ class CruiseLayout(Widget):
|
||||
if has_long or has_icbm:
|
||||
self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad())
|
||||
self.scc_v_toggle.action_item.set_enabled(True)
|
||||
self.scc_m_toggle.action_item.set_enabled(MAPD_AVAILABLE)
|
||||
self.scc_m_toggle.action_item.set_enabled(True)
|
||||
else:
|
||||
ui_state.params.remove("CustomAccIncrementsEnabled")
|
||||
ui_state.params.remove("SmartCruiseControlVision")
|
||||
|
||||
-6
@@ -8,8 +8,6 @@ from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.mapd import MAPD_AVAILABLE
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
|
||||
from openpilot.system.ui.widgets.network import NavButton
|
||||
@@ -45,8 +43,6 @@ class SpeedLimitPolicyLayout(Widget):
|
||||
param="SpeedLimitPolicy",
|
||||
button_width=250,
|
||||
)
|
||||
if not MAPD_AVAILABLE:
|
||||
self._speed_limit_policy.action_item.set_enabled_buttons({Policy.car_state_only})
|
||||
|
||||
items = [
|
||||
self._speed_limit_policy
|
||||
@@ -55,8 +51,6 @@ class SpeedLimitPolicyLayout(Widget):
|
||||
|
||||
@staticmethod
|
||||
def _get_policy_description():
|
||||
if not MAPD_AVAILABLE:
|
||||
return f"<b>{tr('Map data is temporarily unavailable. Using Speed Limit data only from Car.')}</b>"
|
||||
return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS)
|
||||
|
||||
def _render(self, rect):
|
||||
|
||||
@@ -102,13 +102,18 @@ class ModelsLayout(Widget):
|
||||
|
||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||
|
||||
self.blendful_toggle = toggle_item_sp(tr("Blendful"),
|
||||
tr("Nudges the car toward the lane center when the lane lines are clear. Drives laneless when they are not."),
|
||||
param="Blendful")
|
||||
|
||||
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.small_model_item, self.big_model_item, self.cancel_download_item, self.download_item, 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]
|
||||
self.blendful_toggle, 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. " +
|
||||
@@ -235,13 +240,17 @@ class ModelsLayout(Widget):
|
||||
progress = sum(p.progress for p in progresses) / len(progresses)
|
||||
ds = custom.ModelManagerSP.DownloadStatus
|
||||
|
||||
if any(p.canceling for p in progresses):
|
||||
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("canceling...")}
|
||||
if ds.failed in statuses:
|
||||
# close.png is authored black and a tint cannot lift it, hence close2
|
||||
return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"}
|
||||
if ds.verifying in statuses:
|
||||
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("verifying")}
|
||||
if ds.downloading in statuses:
|
||||
return {"name": name, "downloading": True, "progress": progress}
|
||||
speed = max((p.speed for p in progresses), default=0.0)
|
||||
status_text = f"{speed / 1024 ** 2:.1f} MB/s" if speed > 0 else "--.- MB/s"
|
||||
return {"name": name, "downloading": True, "progress": progress, "status_text": status_text}
|
||||
if statuses <= {ds.downloaded, ds.cached}:
|
||||
return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"}
|
||||
# circled_slash is authored grey; tinting it again only darkens it
|
||||
@@ -318,6 +327,7 @@ class ModelsLayout(Widget):
|
||||
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.blendful_toggle.action_item.set_state(ui_state.params.get_bool("Blendful"))
|
||||
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 is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, 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.widgets.button import BigMultiToggle
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
# above 1.0 the pull overshoots lane center and ping-pongs
|
||||
STRENGTHS = {"off": 0.0, "gentle": 0.5, "firm": 1.0}
|
||||
|
||||
|
||||
class LanefulStrengthToggle(BigMultiToggle):
|
||||
"""Tap to cycle LanefulStrength, like the driving personality toggle."""
|
||||
def __init__(self):
|
||||
super().__init__("laneful strength", list(STRENGTHS), select_callback=self._on_select,
|
||||
description="Nudges the car toward lane center.\n" +
|
||||
"Firm centers faster than gentle.")
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
strength = ui_state.params.get("LanefulStrength", return_default=True)
|
||||
self.set_value(min(STRENGTHS, key=lambda k: abs(STRENGTHS[k] - strength)))
|
||||
|
||||
def _on_select(self, value: str):
|
||||
ui_state.params.put("LanefulStrength", STRENGTHS[value])
|
||||
|
||||
|
||||
class LanefulLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._strength = LanefulStrengthToggle()
|
||||
self._scroller.add_widget(self._strength)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._strength.refresh()
|
||||
@@ -11,7 +11,7 @@ import pyray as rl
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||
from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.sunnypilot.model_info import (active_source, big_model_state, bundles_for_source, carrying_model,
|
||||
default_model_name, model_cache_size_mb, model_info, queued_name,
|
||||
@@ -95,7 +95,12 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.clear_cache_btn.set_click_callback(self._confirm_clear_cache)
|
||||
self._cache_size_time = 0.0
|
||||
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn]
|
||||
self.blendful_toggle = BigParamControl("blendful", "Blendful",
|
||||
description="Nudges the car toward the lane center when the lane lines are clear.\n" +
|
||||
"Drives laneless when they are not.")
|
||||
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn,
|
||||
self.blendful_toggle]
|
||||
self._scroller.add_widgets(self.main_items)
|
||||
|
||||
@property
|
||||
@@ -205,6 +210,10 @@ class ModelsLayoutMici(NavScroller):
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self.blendful_toggle.refresh()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
@@ -251,11 +260,13 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.current_model_info.info_text.set_text(tr("download failed"))
|
||||
|
||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
self.cancel_download_btn.set_visible(True)
|
||||
self.cancel_download_btn.set_visible(ui_state.params.get("ModelManager_DownloadRef") is not None)
|
||||
device.set_override_interactive_timeout(5)
|
||||
progress = 0.0
|
||||
count = 0
|
||||
verifying = False
|
||||
canceling = False
|
||||
speed = 0.0
|
||||
for model in manager.selectedBundle.models:
|
||||
count += 1
|
||||
p = model.artifact.downloadProgress
|
||||
@@ -263,11 +274,14 @@ class ModelsLayoutMici(NavScroller):
|
||||
custom.ModelManagerSP.DownloadStatus.verifying):
|
||||
progress += p.progress
|
||||
verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying
|
||||
canceling = canceling or p.canceling
|
||||
speed = max(speed, p.speed) # only one file downloads at a time, so the non-zero speed is the live one
|
||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
|
||||
custom.ModelManagerSP.DownloadStatus.cached):
|
||||
progress += 100.0
|
||||
|
||||
self.current_model_info.current_model_header.set_text(tr("verifying") if verifying else tr("downloading"))
|
||||
header = tr("canceling...") if canceling else tr("verifying") if verifying else tr("downloading")
|
||||
self.current_model_info.current_model_header.set_text(header)
|
||||
self.cancel_download_btn.set_text(tr("cancel verification") if verifying else tr("cancel download"))
|
||||
self.current_model_info.current_model_header._shimmer = True
|
||||
name_text = manager.selectedBundle.internalName.lower()
|
||||
@@ -276,7 +290,10 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.current_model_info.current_model_text.set_text(name_text)
|
||||
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
||||
self.current_model_info.info_header._shimmer = True
|
||||
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
||||
progress_text = f"{progress/count:.2f}%"
|
||||
if not verifying and not canceling:
|
||||
progress_text += f" | {speed / 1024 ** 2:.1f} MB/s" if speed > 0 else " | --.- MB/s"
|
||||
self.current_model_info.info_text.set_text(progress_text)
|
||||
|
||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded:
|
||||
self.current_model_info.info_header.set_text(tr("downloaded"))
|
||||
|
||||
@@ -11,7 +11,6 @@ from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.laneful import LanefulLayoutMici
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
@@ -51,10 +50,6 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
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))
|
||||
|
||||
laneful_panel = LanefulLayoutMici()
|
||||
laneful_btn = SettingsBigButton(tr("laneful"), "", gui_app.texture("icons_mici/settings/device/lkas.png", ICON_SIZE, ICON_SIZE))
|
||||
laneful_btn.set_click_callback(lambda: gui_app.push_widget(laneful_panel))
|
||||
|
||||
# onroad: enable button sits at the front (left of toggles)
|
||||
self._enable_offroad_btn_onroad = BigCircleButton(self.icon_offroad_enable, red=True)
|
||||
self._enable_offroad_btn_onroad.set_click_callback(lambda: self._handle_always_offroad(True))
|
||||
@@ -72,8 +67,7 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
items = self._scroller._items.copy()
|
||||
|
||||
items.insert(1, models_btn)
|
||||
items.insert(2, laneful_btn)
|
||||
items.insert(6, sunnylink_btn)
|
||||
items.insert(5, sunnylink_btn)
|
||||
|
||||
# front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad
|
||||
items.insert(0, self._enable_offroad_btn_onroad)
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, 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.
|
||||
|
||||
Laneful visuals: cyan lane-center target, thin white "after" path (model path bent by laneful's nudge),
|
||||
and a top-center readout of the pull. Mirrors the math in sunnypilot/modeld_v2/laneful.py.
|
||||
"""
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
CENTER_COLOR = (0, 200, 255)
|
||||
AFTER_COLOR = (255, 255, 255)
|
||||
# keep in sync with sunnypilot/modeld_v2/laneful.py
|
||||
LOOKAHEAD_TIME = 1.5
|
||||
MIN_LOOKAHEAD = 10.0
|
||||
MAX_ERROR = 0.5
|
||||
|
||||
|
||||
class LanefulRenderer:
|
||||
def __init__(self):
|
||||
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||
self._after_points = np.empty((0, 2), dtype=np.float32)
|
||||
self._trust = 0.0
|
||||
self._strength = 0.0
|
||||
self._frame = 0
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
# smoothed copies for the readout so the numbers don't flicker
|
||||
self._pull_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||
self._trust_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||
self._readout_rect: rl.Rectangle | None = None
|
||||
self._settings_page = None
|
||||
|
||||
def update(self, renderer, max_idx: int) -> None:
|
||||
"""Called from the model renderer's update with its lane lines and path already in raw points."""
|
||||
if self._frame % 60 == 0:
|
||||
self._strength = ui_state.params.get("LanefulStrength", return_default=True)
|
||||
self._frame += 1
|
||||
|
||||
left, right = renderer._lane_lines[1].raw_points, renderer._lane_lines[2].raw_points
|
||||
stds = list(ui_state.sm['modelV2'].laneLineStds)
|
||||
if self._strength <= 0.0 or len(left) == 0 or len(right) != len(left) or len(stds) < 3:
|
||||
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||
self._after_points = np.empty((0, 2), dtype=np.float32)
|
||||
return
|
||||
|
||||
probs = renderer._lane_line_probs
|
||||
l_prob = float(probs[1] * np.interp(stds[1], [0.15, 0.3], [1.0, 0.0]))
|
||||
r_prob = float(probs[2] * np.interp(stds[2], [0.15, 0.3], [1.0, 0.0]))
|
||||
width = np.clip(right[:, 1] - left[:, 1], 2.8, 4.5) if l_prob * r_prob > 0.5 else 3.7
|
||||
center = left.copy()
|
||||
center[:, 1] = (l_prob * (left[:, 1] + width / 2) + r_prob * (right[:, 1] - width / 2)) / (l_prob + r_prob + 1e-4)
|
||||
self._trust = l_prob + r_prob - l_prob * r_prob
|
||||
self._center_points = renderer._map_line_to_polygon(center, 0.1, 0.0, max_idx)
|
||||
|
||||
# "after" path: model path bent toward center the way laneful's pure-pursuit term steers, ending at the lookahead
|
||||
path = renderer._path.raw_points
|
||||
lookahead = max(ui_state.sm['carState'].vEgo * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||
error = float(np.clip(np.interp(lookahead, center[:, 0], center[:, 1]) - np.interp(lookahead, path[:, 0], path[:, 1]),
|
||||
-MAX_ERROR, MAX_ERROR))
|
||||
self._pull_display.update(self._trust * error) # what laneful actually pulls, 0 when lines are untrusted
|
||||
self._trust_display.update(self._trust)
|
||||
after = path.copy()
|
||||
after[:, 1] += self._strength * self._trust * error * (path[:, 0] / lookahead) ** 2
|
||||
self._after_points = renderer._map_line_to_polygon(after, 0.15, renderer._path_offset_z,
|
||||
renderer._get_path_length_idx(path[:, 0], lookahead))
|
||||
|
||||
def draw(self, rect: rl.Rectangle) -> None:
|
||||
if self._center_points.size and self._trust > 0.05:
|
||||
draw_polygon(rect, self._center_points, rl.Color(*CENTER_COLOR, int(np.clip(self._trust, 0.2, 1.0) * 255)))
|
||||
if self._after_points.size:
|
||||
draw_polygon(rect, self._after_points, rl.Color(*AFTER_COLOR, 200))
|
||||
self._readout_rect = None
|
||||
if self._strength > 0.0:
|
||||
self._draw_readout(rect)
|
||||
|
||||
def handle_tap(self, pos) -> bool:
|
||||
"""Tapping the readout opens the laneful settings page. Returns True if the tap was consumed."""
|
||||
r = self._readout_rect
|
||||
if r is None or not (r.x - 20 <= pos.x <= r.x + r.width + 20 and r.y - 20 <= pos.y <= r.y + r.height + 20):
|
||||
return False
|
||||
if self._settings_page is None:
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.laneful import LanefulLayoutMici
|
||||
self._settings_page = LanefulLayoutMici()
|
||||
gui_app.push_widget(self._settings_page)
|
||||
return True
|
||||
|
||||
def _draw_readout(self, rect: rl.Rectangle) -> None:
|
||||
"""Top-center pill: pull toward lane center in cm (capped at 50), arrow = side,
|
||||
bar grows from center toward the pull (full at the cap), brighter with more lane line trust."""
|
||||
pull, trust = self._pull_display.x, self._trust_display.x
|
||||
w, h = 190, 76
|
||||
x, y = rect.x + (rect.width - w) / 2, rect.y + 10
|
||||
cx, cy = x + w / 2, y + 30
|
||||
self._readout_rect = rl.Rectangle(x, y, w, h)
|
||||
rl.draw_rectangle_rounded(self._readout_rect, 0.5, 12, rl.Color(0, 0, 0, 170))
|
||||
|
||||
text = f"{abs(pull) * 100:.0f}"
|
||||
text_w = measure_text_cached(self._font, text, 44, 0).x
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(cx - text_w / 2, cy - 24), 44, 0, rl.WHITE)
|
||||
|
||||
active = rl.Color(*CENTER_COLOR, int(np.clip(abs(pull) / 0.1, 0.0, 1.0) * 255))
|
||||
idle = rl.Color(255, 255, 255, 40)
|
||||
lx, rx = x + 18, x + w - 18
|
||||
rl.draw_triangle(rl.Vector2(lx, cy), rl.Vector2(lx + 16, cy + 12), rl.Vector2(lx + 16, cy - 12), active if pull < 0 else idle)
|
||||
rl.draw_triangle(rl.Vector2(rx - 16, cy - 12), rl.Vector2(rx - 16, cy + 12), rl.Vector2(rx, cy), active if pull > 0 else idle)
|
||||
|
||||
bar = rl.Rectangle(x + 24, y + h - 16, w - 48, 6)
|
||||
rl.draw_rectangle_rounded(bar, 1.0, 6, rl.Color(255, 255, 255, 40))
|
||||
rl.draw_rectangle(int(cx - 1), int(bar.y - 2), 2, int(bar.height + 4), rl.Color(255, 255, 255, 120))
|
||||
fill = bar.width / 2 * float(np.clip(abs(pull) / MAX_ERROR, 0.0, 1.0))
|
||||
if fill > 1:
|
||||
fill_x = cx if pull > 0 else cx - fill
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(fill_x, bar.y, fill, bar.height), 1.0, 6,
|
||||
rl.Color(*CENTER_COLOR, int(np.clip(0.3 + 0.7 * trust, 0.0, 1.0) * 255)))
|
||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import UIStatus
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.laneful import LanefulRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.blendful import BlendfulRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||
|
||||
LANE_LINE_COLORS_SP = {
|
||||
@@ -18,4 +18,4 @@ LANE_LINE_COLORS_SP = {
|
||||
class ModelRendererSP:
|
||||
def __init__(self):
|
||||
self.rainbow_path = RainbowPath()
|
||||
self.laneful = LanefulRenderer()
|
||||
self.blendful = BlendfulRenderer()
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Copyright (c) 2021-, 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 numpy as np
|
||||
import pyray as rl
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.modeld_v2.blendful import LANE_WIDTH_CLIP, LOOKAHEAD_TIME, MIN_LOOKAHEAD
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
BLENDFUL_COLOR = (0, 200, 255)
|
||||
FULL_BAR = 1.0
|
||||
|
||||
|
||||
class BlendfulRenderer:
|
||||
def __init__(self, readout: bool = True):
|
||||
self._readout = readout
|
||||
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||
self._trust = 0.0
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
self._pull_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||
self._trust_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||
self._alpha = FirstOrderFilter(0.0, 0.3, DT_MDL)
|
||||
|
||||
def update(self, renderer, max_idx: int, *map_args) -> None:
|
||||
blendful = ui_state.sm['modelDataV2SP']
|
||||
if ui_state.sm.updated['modelDataV2SP']:
|
||||
self._alpha.update(float(blendful.blendfulTrust > 0.0))
|
||||
if blendful.blendfulTrust > 0.0:
|
||||
self._trust = blendful.blendfulTrust
|
||||
if self._readout:
|
||||
lookahead = max(ui_state.sm['carState'].vEgo * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||
self._pull_display.update(blendful.blendfulCurvature * lookahead ** 2 / 2)
|
||||
self._trust_display.update(self._trust)
|
||||
|
||||
left, right = renderer._lane_lines[1].raw_points, renderer._lane_lines[2].raw_points
|
||||
stds = list(ui_state.sm['modelV2'].laneLineStds)
|
||||
if self._alpha.x < 0.01 or len(left) == 0 or len(right) != len(left) or len(stds) < 3:
|
||||
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||
return
|
||||
|
||||
if blendful.blendfulTrust > 0.0:
|
||||
l_prob, r_prob = blendful.blendfulLeftProb, blendful.blendfulRightProb
|
||||
half_width = np.clip(right[:, 1] - left[:, 1], *LANE_WIDTH_CLIP) / 2 if l_prob * r_prob > 0.5 else blendful.blendfulLaneWidth / 2
|
||||
l_var = stds[1] ** 2 / max(l_prob, 1e-4)
|
||||
r_var = stds[2] ** 2 / max(r_prob, 1e-4)
|
||||
center = left.copy()
|
||||
center[:, 1] = (r_var * (left[:, 1] + half_width) + l_var * (right[:, 1] - half_width)) / (l_var + r_var)
|
||||
self._center_points = renderer._map_line_to_polygon(center, 0.15, 0.0, max_idx, *map_args)
|
||||
|
||||
def _color(self, rgb, alpha: float) -> rl.Color:
|
||||
return rl.Color(*rgb, int(alpha * self._alpha.x))
|
||||
|
||||
def draw(self, rect: rl.Rectangle) -> None:
|
||||
if self._center_points.size and self._trust > 0.05:
|
||||
draw_polygon(rect, self._center_points, self._color(BLENDFUL_COLOR, np.clip(self._trust, 0.2, 1.0) * 255))
|
||||
if self._center_points.size and self._readout:
|
||||
self._draw_readout(rect)
|
||||
|
||||
def _draw_readout(self, rect: rl.Rectangle) -> None:
|
||||
pull, trust = self._pull_display.x, self._trust_display.x
|
||||
w, h = 190, 76
|
||||
x, y = rect.x + (rect.width - w) / 2, rect.y + 10
|
||||
cx, cy = x + w / 2, y + 30
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(x, y, w, h), 0.5, 12, self._color((0, 0, 0), 170))
|
||||
|
||||
text = f"{abs(pull) * 100:.0f}"
|
||||
text_w = measure_text_cached(self._font, text, 44, 0).x
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(cx - text_w / 2, cy - 24), 44, 0, self._color((255, 255, 255), 255))
|
||||
|
||||
active = self._color(BLENDFUL_COLOR, np.clip(abs(pull) / 0.1, 0.0, 1.0) * 255)
|
||||
idle = self._color((255, 255, 255), 40)
|
||||
lx, rx = x + 18, x + w - 18
|
||||
rl.draw_triangle(rl.Vector2(lx, cy), rl.Vector2(lx + 16, cy + 12), rl.Vector2(lx + 16, cy - 12), active if pull < 0 else idle)
|
||||
rl.draw_triangle(rl.Vector2(rx - 16, cy - 12), rl.Vector2(rx - 16, cy + 12), rl.Vector2(rx, cy), active if pull > 0 else idle)
|
||||
|
||||
bar = rl.Rectangle(x + 24, y + h - 16, w - 48, 6)
|
||||
rl.draw_rectangle_rounded(bar, 1.0, 6, self._color((255, 255, 255), 40))
|
||||
rl.draw_rectangle(int(cx - 1), int(bar.y - 2), 2, int(bar.height + 4), self._color((255, 255, 255), 120))
|
||||
fill = bar.width / 2 * float(np.sqrt(np.clip(abs(pull) / FULL_BAR, 0.0, 1.0)))
|
||||
if fill > 1:
|
||||
fill_x = cx if pull > 0 else cx - fill
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(fill_x, bar.y, fill, bar.height), 1.0, 6,
|
||||
self._color(BLENDFUL_COLOR, np.clip(0.3 + 0.7 * trust, 0.0, 1.0) * 255))
|
||||
@@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.blendful import BlendfulRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import MADSState
|
||||
@@ -16,6 +17,7 @@ class ModelRendererSP:
|
||||
def __init__(self):
|
||||
self.rainbow_path = RainbowPath()
|
||||
self.chevron_metrics = ChevronMetrics()
|
||||
self.blendful = BlendfulRenderer(readout=False)
|
||||
self._width_filter = FirstOrderFilter(0.9, 0.1, 1 / gui_app.target_fps)
|
||||
|
||||
@property
|
||||
|
||||
@@ -36,7 +36,7 @@ class UIStateSP:
|
||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||
self.sm_services_ext = [
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay"
|
||||
"lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay", "modelDataV2SP"
|
||||
]
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
|
||||
@@ -3,7 +3,3 @@ from openpilot.common.basedir import BASEDIR
|
||||
|
||||
MAPD_BIN_DIR = os.path.join(BASEDIR, 'openpilot', 'third_party/mapd_pfeiferj')
|
||||
MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd')
|
||||
|
||||
# mapd still runs (OSM downloads) but has no position source since ubloxd owns LastGPSPosition;
|
||||
# map-based features stay off until mapd is rebuilt
|
||||
MAPD_AVAILABLE = False
|
||||
|
||||
@@ -4,6 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import platform
|
||||
|
||||
@@ -26,6 +27,21 @@ class OsmMapData(BaseMapData):
|
||||
self.last_bearing = math.degrees(location.calibratedOrientationNED.value[2])
|
||||
self.last_position = Coordinate(location.positionGeodetic.value[0], location.positionGeodetic.value[1])
|
||||
|
||||
if self.last_position is None:
|
||||
return
|
||||
|
||||
params = {
|
||||
"latitude": self.last_position.latitude,
|
||||
"longitude": self.last_position.longitude,
|
||||
}
|
||||
|
||||
if self.last_bearing is not None:
|
||||
params['bearing'] = self.last_bearing
|
||||
|
||||
# mapd reads its position from the memory params as JSON, so write it for the mapd binary.
|
||||
# LastGPSPosition is registered as BYTES (ubloxd owns the persistent copy for pigeond),
|
||||
# so the JSON payload has to be encoded here.
|
||||
self.mem_params.put("LastGPSPosition", json.dumps(params).encode(), block=True)
|
||||
|
||||
def get_current_speed_limit(self) -> float:
|
||||
return float(self.mem_params.get("MapSpeedLimit") or 0.0)
|
||||
|
||||
@@ -121,6 +121,10 @@ def update_osm_db() -> None:
|
||||
if not mem_params.get("OSMDownloadBounds"):
|
||||
mem_params.put("OSMDownloadBounds", "", block=True)
|
||||
|
||||
# mapd reads LastGPSPosition from the memory params as JSON, and it is registered as BYTES upstream
|
||||
if not mem_params.get("LastGPSPosition"):
|
||||
mem_params.put("LastGPSPosition", b"{}", block=True)
|
||||
|
||||
|
||||
def main_thread():
|
||||
update_installed_version(VERSION, params)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import platform
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData
|
||||
|
||||
|
||||
class TestOsmMapData(OpenpilotTestCase):
|
||||
"""The mapd binary reads its position from the memory params as JSON, so mapd_manager has to feed it."""
|
||||
|
||||
LATITUDE = 32.875134
|
||||
LONGITUDE = -117.209777
|
||||
ALTITUDE = 42.0
|
||||
BEARING = 90.0
|
||||
|
||||
def setup_method(self):
|
||||
self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else Params()
|
||||
self.mem_params.remove("LastGPSPosition")
|
||||
self.map_data = OsmMapData()
|
||||
self.pm = messaging.PubMaster(['liveLocationKalman'])
|
||||
|
||||
def location_message(self, status: log.LiveLocationKalman.Status, position_valid: bool):
|
||||
msg = messaging.new_message('liveLocationKalman', valid=True)
|
||||
location = msg.liveLocationKalman
|
||||
location.status = status
|
||||
location.positionGeodetic = {'value': [self.LATITUDE, self.LONGITUDE, self.ALTITUDE], 'valid': position_valid}
|
||||
location.calibratedOrientationNED = {'value': [0.0, 0.0, math.radians(self.BEARING)], 'valid': True}
|
||||
return msg
|
||||
|
||||
def send_location(self, status: log.LiveLocationKalman.Status, position_valid: bool) -> None:
|
||||
# the first exchange synchronizes the PUB/SUB connection, the second one is actually received
|
||||
self.pm.send('liveLocationKalman', self.location_message(status, position_valid))
|
||||
self.map_data.sm.update(1000)
|
||||
self.pm.send('liveLocationKalman', self.location_message(status, position_valid))
|
||||
self.map_data.sm.update(1000)
|
||||
|
||||
def read_position(self) -> dict:
|
||||
value = self.mem_params.get("LastGPSPosition")
|
||||
self.assertIsNotNone(value, "LastGPSPosition was not written for mapd")
|
||||
return json.loads(value)
|
||||
|
||||
def test_writes_position_for_mapd(self):
|
||||
self.send_location(log.LiveLocationKalman.Status.valid, True)
|
||||
self.map_data.tick()
|
||||
|
||||
position = self.read_position()
|
||||
self.assertAlmostEqual(position['latitude'], self.LATITUDE, delta=1e-6)
|
||||
self.assertAlmostEqual(position['longitude'], self.LONGITUDE, delta=1e-6)
|
||||
self.assertAlmostEqual(position['bearing'], self.BEARING, delta=1e-6)
|
||||
|
||||
def test_invalid_localizer_keeps_last_known_position(self):
|
||||
self.send_location(log.LiveLocationKalman.Status.valid, True)
|
||||
self.map_data.tick()
|
||||
|
||||
self.send_location(log.LiveLocationKalman.Status.uninitialized, False)
|
||||
self.map_data.tick()
|
||||
|
||||
position = self.read_position()
|
||||
self.assertAlmostEqual(position['latitude'], self.LATITUDE, delta=1e-6)
|
||||
self.assertAlmostEqual(position['longitude'], self.LONGITUDE, delta=1e-6)
|
||||
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
|
||||
|
||||
LANE_WIDTH_DEFAULT = 3.6 # m
|
||||
LANE_WIDTH_CLIP = (2.8, 4.5) # m
|
||||
LOOKAHEAD_TIME = 1.5 # s
|
||||
MIN_LOOKAHEAD = 10.0 # m
|
||||
MIN_SPEED = 5.0 # m/s
|
||||
LINE_STD = (0.2, 0.6)
|
||||
POINT_STD = 0.1
|
||||
YIELD = (0.2, 0.45)
|
||||
X_IDXS = np.array(ModelConstants.X_IDXS)
|
||||
|
||||
|
||||
class Blendful:
|
||||
def __init__(self):
|
||||
self.lane_width = FirstOrderFilter(LANE_WIDTH_DEFAULT, 10.0, DT_MDL, initialized=False)
|
||||
self.line_gone = FirstOrderFilter(np.zeros(2), 2.0, DT_MDL)
|
||||
self.line_probs_slow = FirstOrderFilter(np.zeros(2), 3.0, DT_MDL)
|
||||
self.line_probs = FirstOrderFilter(np.zeros(2), 0.5, DT_MDL)
|
||||
self.usual_offset = FirstOrderFilter(0.0, 10.0, DT_MDL, initialized=False)
|
||||
self.going_around = False
|
||||
self.since_turning = 0.0
|
||||
self.trust = 0.0
|
||||
self.line_trust = 0.0
|
||||
self.dk = 0.0 # delta kappa; difference between lane center and model plan
|
||||
self.l_prob = 0.0
|
||||
self.r_prob = 0.0
|
||||
self.model_curvature = 0.0
|
||||
self.smooth = 0.0 # smoothens lanelines below .53 lat accel
|
||||
self.curvature = 0.0
|
||||
|
||||
def update(self, model_output: dict[str, np.ndarray], v_ego: float, active: bool, model_curvature: float) -> float:
|
||||
self.l_prob = self.r_prob = 0.0
|
||||
fading = not active or v_ego < MIN_SPEED
|
||||
if fading:
|
||||
self.trust = self.line_trust = 0.0
|
||||
self.usual_offset.initialized = False
|
||||
self.lane_width.initialized = False
|
||||
self.going_around = False
|
||||
self.model_curvature = model_curvature
|
||||
target = 0.0 if fading else self._target(model_output, v_ego, model_curvature)
|
||||
target = self.smooth = self.smooth + (target - self.smooth) * DT_MDL / (DT_MDL + np.interp(abs(model_curvature) * v_ego ** 2, [0.18, 0.53], [0.25, 0.0]))
|
||||
build = 0.5 / max(v_ego, 1.0) ** 2 * DT_MDL # jerk over 1 second
|
||||
release = (0.5 if fading else 2.0) / max(v_ego, 1.0) ** 2 * DT_MDL # lets go fast, but past zero it builds like any other pull
|
||||
k = self.curvature
|
||||
self.curvature = float(np.clip(target, max(k - release, -build) if k > 0 else k - build, min(k + release, build) if k < 0 else k + build))
|
||||
return self.curvature
|
||||
|
||||
def _target(self, model_output: dict[str, np.ndarray], v_ego: float, model_curvature: float) -> float:
|
||||
lookahead = max(v_ego * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||
lines = model_output['lane_lines'][0, :, :, 0]
|
||||
stds = model_output['lane_lines_stds'][0, :, :, 0]
|
||||
|
||||
lone_weight = np.interp(abs(model_curvature) * v_ego ** 2, [0.18, 0.53], [0.5, 1.0]) # 0.5 on straights, 1.0 in curves
|
||||
probs = self.line_probs.update(model_output['lane_lines_prob'][0, 1::2][1:3])
|
||||
absent = (probs < 0.3) & (lone_weight > 0.5)
|
||||
gone = self.line_gone.update((model_output['lane_lines_prob'][0, 1::2][1:3] < 0.3).astype(float))
|
||||
missing = np.where(absent, 1.0, gone)
|
||||
dropping = bool(np.any((probs < 0.6 * self.line_probs_slow.update(model_output['lane_lines_prob'][0, 1::2][1:3])) & ~absent))
|
||||
l_prob = float(probs[0] * np.interp(np.interp(0.5 * lookahead, X_IDXS, stds[1]), LINE_STD, [1.0, 0.0]))
|
||||
r_prob = float(probs[1] * np.interp(np.interp(0.5 * lookahead, X_IDXS, stds[2]), LINE_STD, [1.0, 0.0]))
|
||||
self.l_prob, self.r_prob = l_prob, r_prob
|
||||
# one line steadily on its own still centers at lone_weight. a line that is disappearing gives control back to laneless
|
||||
if missing[1] > 0.8 and l_prob > 0.5:
|
||||
lane_trust = l_prob * lone_weight
|
||||
elif missing[0] > 0.8 and r_prob > 0.5:
|
||||
lane_trust = r_prob * lone_weight
|
||||
elif dropping:
|
||||
lane_trust = min(l_prob, r_prob)
|
||||
else:
|
||||
lane_trust = l_prob + r_prob - l_prob * r_prob
|
||||
if lone_weight > 0.5 and lane_trust < self.line_trust:
|
||||
lane_trust = self.line_trust + (lane_trust - self.line_trust) * DT_MDL / (lone_weight - 0.5 + DT_MDL)
|
||||
self.line_trust = lane_trust
|
||||
if lane_trust <= 0.0:
|
||||
self.trust = 0.0
|
||||
return 0.0
|
||||
if l_prob + r_prob <= 0.0: # keep the last fit while trust lets go
|
||||
self.trust = min(self.trust, lane_trust)
|
||||
return float(self.trust * self.dk)
|
||||
|
||||
win = (X_IDXS >= 0.5 * lookahead) & (X_IDXS <= 1.5 * lookahead)
|
||||
x = X_IDXS[win]
|
||||
half_width = self.lane_width.x / 2
|
||||
if l_prob * r_prob > 0.5:
|
||||
width = np.interp(lookahead, X_IDXS, lines[2]) - np.interp(lookahead, X_IDXS, lines[1])
|
||||
self.lane_width.update(float(np.clip(width, *LANE_WIDTH_CLIP)))
|
||||
half_width = np.clip(lines[2, win] - lines[1, win], *LANE_WIDTH_CLIP) / 2
|
||||
l_var = stds[1, win] ** 2 / max(l_prob, 1e-4)
|
||||
r_var = stds[2, win] ** 2 / max(r_prob, 1e-4)
|
||||
center = (r_var * (lines[1, win] + half_width) + l_var * (lines[2, win] - half_width)) / (l_var + r_var)
|
||||
weight = 1.0 / (l_var * r_var / (l_var + r_var) + POINT_STD ** 2)
|
||||
|
||||
# Fit curvature to place the plant in center: e ~ dk x^2 / 2
|
||||
plan_x = np.interp(x, model_output['plan'][0, :, 0], model_output['plan'][0, :, 1])
|
||||
plan_share = np.interp(abs(model_curvature) * lookahead ** 2 / 2, [0.5, 2.0], [0.3, 0.5]) # blending up to 50% plan with action during sharp curvature
|
||||
error = np.clip(center - ((1 - plan_share) * model_curvature * x ** 2 / 2 + plan_share * plan_x), -1.0, 1.0)
|
||||
dk = 2.0 * np.sum(weight * error * x ** 2) / np.sum(weight * x ** 4)
|
||||
|
||||
# A model planning to end up further from its usual spot in the lane than the plant is now is likely going around something
|
||||
hw_now = np.clip(lines[2, 0] - lines[1, 0], *LANE_WIDTH_CLIP) / 2 if l_prob * r_prob > 0.5 else self.lane_width.x / 2
|
||||
center_now = (l_prob * (lines[1, 0] + hw_now) + r_prob * (lines[2, 0] - hw_now)) / (l_prob + r_prob)
|
||||
plan_y = np.interp(lookahead, model_output['plan'][0, :, 0], model_output['plan'][0, :, 1])
|
||||
offset = plan_y - np.interp(lookahead, x, center)
|
||||
if min(gone) > 0.5: # both lines gone: turns, intersections, relearn ideal model position
|
||||
self.usual_offset.initialized = False
|
||||
usual = self.usual_offset.x if self.usual_offset.initialized else offset
|
||||
swerve = abs(offset - usual) - 0.25 * abs(model_curvature) * lookahead ** 2 / 2
|
||||
away = swerve - abs(-center_now - usual)
|
||||
lane_curvature = 2.0 * np.polyfit(x, center, 2)[0]
|
||||
inside_curve = abs(lane_curvature) * lookahead ** 2 / 2 > 0.1 and (offset - usual) * lane_curvature > 0
|
||||
self.since_turning = 0.0 if abs(model_curvature) * lookahead ** 2 / 2 > 0.5 else self.since_turning + DT_MDL
|
||||
if inside_curve:
|
||||
self.going_around = False
|
||||
elif away > YIELD[0] and self.since_turning > 2.0:
|
||||
self.going_around = True
|
||||
elif swerve < YIELD[0] / 2:
|
||||
self.going_around = False
|
||||
self.usual_offset.update_alpha(20.0 if self.going_around else 10.0)
|
||||
self.usual_offset.update(offset)
|
||||
if inside_curve:
|
||||
lane_trust *= np.interp(abs(offset) - abs(center_now) - 0.25 * abs(model_curvature) * lookahead ** 2 / 2, (0.5, 1.0), [1.0, 0.0])
|
||||
else:
|
||||
lane_trust *= np.interp(swerve, (YIELD[0] / 2, YIELD[0]), [1.0, 0.0]) if self.going_around else np.interp(away, YIELD, [1.0, 0.0])
|
||||
|
||||
self.trust = lane_trust if lane_trust < self.trust else self.trust + (lane_trust - self.trust) * DT_MDL / (1.5 + DT_MDL)
|
||||
self.dk = dk
|
||||
return float(self.trust * dk)
|
||||
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import argparse
|
||||
import dataclasses
|
||||
import inspect
|
||||
import itertools
|
||||
import mmap
|
||||
import re
|
||||
@@ -45,6 +46,11 @@ def _address(v):
|
||||
return _root(v.src[0]), add + start, size
|
||||
|
||||
|
||||
def _buffer(u):
|
||||
from tinygrad.uop.ops import Ops
|
||||
return u.buffer if u.op is Ops.BUFFER else getattr(u.arg, "buffer", None)
|
||||
|
||||
|
||||
def _patched(buf, topo) -> tuple[bytearray, dict]:
|
||||
from tinygrad.uop.ops import Ops
|
||||
blob, rows = bytearray(buf.max_numel() * buf.dtype.itemsize), {}
|
||||
@@ -72,7 +78,7 @@ def _evaluate(u, env):
|
||||
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env)
|
||||
return struct.unpack_from(u.dtype.fmt, table.arg, index * u.dtype.itemsize)[0] if table.op is Ops.BINARY and index is not None else None
|
||||
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env) for x in u.src]):
|
||||
return exec_alu(u.op, u.dtype, values, False)
|
||||
return exec_alu(u.op, u.dtype, values, *([False] if "truncate_output" in inspect.signature(exec_alu).parameters else []))
|
||||
return None
|
||||
|
||||
|
||||
@@ -112,7 +118,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
||||
from tinygrad.uop.ops import Ops
|
||||
submit = jit.captured._linear.src[0].without_after
|
||||
kernels, topo = submit.arg.aux.kernels, list(jit.captured._linear.toposort(enter_calls=True))
|
||||
allocs = {str(u.tag): u for u in topo if u.op is Ops.ALLOC and u.tag is not None}
|
||||
allocs = {str(name): u for u in topo if u.op is Ops.ALLOC and (name := getattr(u, "tag", None) or u.arg.name) is not None}
|
||||
(commands, patched), kernargs = _patched(allocs["cmdbuf_compute_0"], topo), allocs["kernargs_compute_0"]
|
||||
argument_words, pokes = _patched(kernargs, topo)[1], _input_pokes(submit, kernargs)
|
||||
sh = {name: getattr(amd_gpu, f"reg{name}") + amd_gpu.GC_BASE__INST0_SEG0 - pm4.PACKET3_SET_SH_REG_START
|
||||
@@ -158,7 +164,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
||||
args.append({"input": pokes[slot][0]})
|
||||
elif slot in argument_words:
|
||||
base, at, nbytes = _address(argument_words[slot])
|
||||
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.arg.buffer.nbytes, "dtype": u8})
|
||||
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.max_numel() * base.dtype.itemsize, "dtype": u8})
|
||||
outs, ins = kernel[6]
|
||||
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": dataclasses.asdict(target),
|
||||
"signature": [[f"data{i}", i, u8, [a.get("nbytes", 1)]] for i, a in enumerate(args)], "global_size": grid, "local_size": local,
|
||||
@@ -166,12 +172,13 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
||||
needs_init |= {args[i]["buffer"] for i in ins if "buffer" in args[i] and args[i]["buffer"] not in written}
|
||||
written |= {args[i]["buffer"] for i in outs if "buffer" in args[i]}
|
||||
|
||||
assert all(_buffer(b) is not None for b, i in buffers.items() if i in needs_init), "a kernel reads a scratch before any kernel writes it"
|
||||
names = list(jit.captured.expected_names)
|
||||
inputs = [{"slot": slot, "name": names[slot] if slot < len(names) else None, "dtype": repr(u.dtype), "numel": int(u.max_numel()), "device": u.device}
|
||||
for slot, u in enumerate(_prepare_jit_inputs((), kwargs)[0])]
|
||||
header = {"input_names": names, "calls": calls, "returns": {"kind": "NoneType", "tensors": []}, "inputs": inputs,
|
||||
"buffers": [{"nbytes": b.arg.buffer.nbytes, "device": b.arg.buffer.device, "dtype": u8}
|
||||
| ({"init": blob.add(b.arg.buffer.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
|
||||
"buffers": [{"nbytes": bb.nbytes if (bb := _buffer(b)) else b.max_numel() * b.dtype.itemsize, "device": bb.device if bb else b.device, "dtype": u8}
|
||||
| ({"init": blob.add(bb.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
|
||||
"check": {"inputs": [[path, store(array), device] for path, array, device in frame["inputs"]],
|
||||
"outputs": [store(array) for array in frame["outputs"]], "output_container": frame["output_container"]}}
|
||||
with sink:
|
||||
|
||||
@@ -250,6 +250,12 @@ def _allocate_buffers(header: dict, blob: memoryview, device_map: dict | None, s
|
||||
return buffers
|
||||
|
||||
|
||||
def _fields(what, /, **values): # a tinygrad dataclass built or replaced with the fields it has today: a dropped field is fine, a new required one is not
|
||||
known = {field.name for field in dataclasses.fields(what)}
|
||||
values = {name: value for name, value in values.items() if name in known}
|
||||
return what(**values) if isinstance(what, type) else dataclasses.replace(what, **values)
|
||||
|
||||
|
||||
def _program_uop(call: dict, lib: bytes):
|
||||
from tinygrad.helpers import Target
|
||||
from tinygrad.uop.ops import KernelInfo, Ops, ProgramInfo, UOp
|
||||
@@ -257,9 +263,9 @@ def _program_uop(call: dict, lib: bytes):
|
||||
for name, compact_slot, dtype, shape in call["signature"]:
|
||||
parameter = UOp.param(call["globals"][compact_slot], _dtype(dtype), math.prod(shape))
|
||||
image = (shape[0], shape[1]) if len(shape) == 3 and shape[2] == 4 else None
|
||||
parameters.append(parameter.replace(arg=dataclasses.replace(parameter.arg, name=name, image=image)))
|
||||
info = ProgramInfo(global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), vars=(), globals=tuple(call["globals"]),
|
||||
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=Target(**call["target"]))
|
||||
parameters.append(parameter.replace(arg=_fields(parameter.arg, name=name, image=image)))
|
||||
info = _fields(ProgramInfo, global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), globals=tuple(call["globals"]),
|
||||
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=_fields(Target, **call["target"]))
|
||||
return UOp(Ops.PROGRAM, src=(UOp(Ops.SINK, arg=KernelInfo(name=call["name"])), UOp(Ops.LINEAR, src=tuple(parameters)),
|
||||
UOp(Ops.SOURCE, arg=""), UOp(Ops.BINARY, arg=lib)), arg=info)
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, 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.
|
||||
|
||||
Laneful: nudge the model's curvature toward the lane center seen by the lane lines.
|
||||
The model stays in charge; this only adds a pure-pursuit correction weighted by lane line confidence,
|
||||
and fades to zero (plain laneless) when the lines are weak.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan
|
||||
|
||||
LANE_WIDTH_DEFAULT = 3.7 # m
|
||||
LANE_WIDTH_CLIP = (2.8, 4.5) # m
|
||||
LOOKAHEAD_TIME = 1.5 # s
|
||||
MIN_LOOKAHEAD = 10.0 # m
|
||||
MAX_ERROR = 0.5 # m, cap on how far laneful can pull
|
||||
MIN_SPEED = 9.0 # m/s
|
||||
|
||||
|
||||
class Laneful:
|
||||
def __init__(self):
|
||||
self.lane_width = FirstOrderFilter(LANE_WIDTH_DEFAULT, 10.0, DT_MDL)
|
||||
|
||||
def update(self, model_output: dict[str, np.ndarray], v_ego: float, strength: float, desire_active: bool) -> float:
|
||||
"""Returns a curvature offset to add to the model's raw desired curvature."""
|
||||
if strength <= 0.0 or desire_active or v_ego < MIN_SPEED or 'lane_lines' not in model_output:
|
||||
return 0.0
|
||||
|
||||
lookahead = max(v_ego * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||
lines = model_output['lane_lines'][0, :, :, 0]
|
||||
left_y = np.interp(lookahead, ModelConstants.X_IDXS, lines[1])
|
||||
right_y = np.interp(lookahead, ModelConstants.X_IDXS, lines[2])
|
||||
|
||||
# line confidence, discounted when the model is unsure where the line is
|
||||
probs = model_output['lane_lines_prob'][0, 1::2]
|
||||
stds = model_output['lane_lines_stds'][0, :, 0, 0]
|
||||
l_prob = float(probs[1] * np.interp(stds[1], [0.15, 0.3], [1.0, 0.0]))
|
||||
r_prob = float(probs[2] * np.interp(stds[2], [0.15, 0.3], [1.0, 0.0]))
|
||||
|
||||
if l_prob * r_prob > 0.5:
|
||||
self.lane_width.update(float(np.clip(right_y - left_y, *LANE_WIDTH_CLIP)))
|
||||
half_width = self.lane_width.x / 2
|
||||
|
||||
# one weak line -> hug the strong one at half a lane width
|
||||
center_y = (l_prob * (left_y + half_width) + r_prob * (right_y - half_width)) / (l_prob + r_prob + 1e-4)
|
||||
lane_trust = l_prob + r_prob - l_prob * r_prob
|
||||
|
||||
plan = model_output['plan'][0, :, Plan.POSITION]
|
||||
plan_y = np.interp(lookahead, plan[:, 0], plan[:, 1])
|
||||
|
||||
error = np.clip(center_y - plan_y, -MAX_ERROR, MAX_ERROR)
|
||||
return float(strength * lane_trust * 2.0 * error / lookahead ** 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def fake_output(left_y, right_y, prob=0.9, std=0.1, plan_y=0.0):
|
||||
x = np.array(ModelConstants.X_IDXS)
|
||||
lines = np.zeros((1, 4, len(x), 2), dtype=np.float32)
|
||||
lines[0, 1, :, 0], lines[0, 2, :, 0] = left_y, right_y
|
||||
plan = np.zeros((1, len(x), 15), dtype=np.float32)
|
||||
plan[0, :, 0], plan[0, :, 1] = x, plan_y
|
||||
return {'lane_lines': lines, 'plan': plan,
|
||||
'lane_lines_prob': np.full((1, 8), prob, dtype=np.float32),
|
||||
'lane_lines_stds': np.full((1, 4, len(x), 2), std, dtype=np.float32)}
|
||||
|
||||
lf = Laneful()
|
||||
assert abs(lf.update(fake_output(-1.85, 1.85), 25.0, 1.0, False)) < 1e-6, "centered -> no nudge"
|
||||
assert lf.update(fake_output(-1.0, 2.7), 25.0, 1.0, False) > 0, "lane center to the +y side -> nudge toward +y"
|
||||
assert lf.update(fake_output(-2.7, 1.0), 25.0, 1.0, False) < 0, "lane center to the -y side -> nudge toward -y"
|
||||
assert lf.update(fake_output(-1.0, 2.7), 25.0, 1.0, True) == 0.0, "off during lane change/turn"
|
||||
assert lf.update(fake_output(-1.0, 2.7, std=0.5), 25.0, 1.0, False) == 0.0, "blurry lines -> laneless"
|
||||
assert lf.update(fake_output(-1.0, 2.7), 25.0, 0.0, False) == 0.0, "strength 0 -> off"
|
||||
assert lf.update(fake_output(-1.0, 2.7), 25.0, 0.5, False) < lf.update(fake_output(-1.0, 2.7), 25.0, 1.0, False)
|
||||
print("laneful self-check ok")
|
||||
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import gc
|
||||
import os
|
||||
os.environ['GMMU'] = '0'
|
||||
@@ -39,7 +40,7 @@ from openpilot.selfdrive.modeld.modeld import ChestnutGpuState
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser
|
||||
from openpilot.sunnypilot.modeld_v2.laneful import Laneful
|
||||
from openpilot.sunnypilot.modeld_v2.blendful import Blendful
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan
|
||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
@@ -230,7 +231,7 @@ class ModelState(ModelStateBase):
|
||||
|
||||
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||
lat_action_t: float, long_action_t: float, v_ego: float,
|
||||
laneful_curvature: float = 0.0) -> log.ModelDataV2.Action:
|
||||
blendful: Callable[[float], float] | None = None) -> log.ModelDataV2.Action:
|
||||
if 'action' not in model_output:
|
||||
plan = model_output['plan'][0]
|
||||
desired_accel = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||
@@ -242,7 +243,8 @@ class ModelState(ModelStateBase):
|
||||
else:
|
||||
desired_accel = model_output['action'][0, 1]
|
||||
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
|
||||
desired_curvature += laneful_curvature
|
||||
if blendful is not None:
|
||||
desired_curvature += blendful(desired_curvature)
|
||||
|
||||
stop = v_ego < 0.3 and desired_accel < 0.1
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
@@ -369,8 +371,8 @@ def main(demo=False):
|
||||
DH = DesireHelper()
|
||||
meta_constants = load_meta_constants()
|
||||
RELC = RoadEdgeLaneChangeController()
|
||||
laneful = Laneful()
|
||||
laneful_strength = 0.0
|
||||
blendful = Blendful()
|
||||
blendful_enabled = False
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
@@ -414,7 +416,7 @@ def main(demo=False):
|
||||
model.lat_delay = get_lat_delay(params, sm["lateralDelay"].lateralDelay)
|
||||
model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
laneful_strength = params.get("LanefulStrength", return_default=True)
|
||||
blendful_enabled = params.get_bool("Blendful")
|
||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||
if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32)
|
||||
@@ -488,9 +490,9 @@ def main(demo=False):
|
||||
|
||||
if not sm['carControl'].latActive:
|
||||
prev_action.desiredCurvature = sm['controlsState'].curvature
|
||||
laneful_curvature = laneful.update(model_output, v_ego, laneful_strength,
|
||||
desire != log.Desire.none or DH.lane_change_state != log.LaneChangeState.off)
|
||||
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego, laneful_curvature)
|
||||
blendful_active = blendful_enabled and desire == log.Desire.none and DH.lane_change_state == log.LaneChangeState.off
|
||||
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego,
|
||||
partial(blendful.update, model_output, v_ego, blendful_active))
|
||||
prev_action = action
|
||||
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
||||
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
||||
@@ -507,6 +509,13 @@ def main(demo=False):
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
mdv2sp_send.valid = modelv2_send.valid
|
||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||
mdv2sp_send.modelDataV2SP.blendfulCurvature = blendful.curvature
|
||||
mdv2sp_send.modelDataV2SP.blendfulTrust = float(blendful.trust)
|
||||
mdv2sp_send.modelDataV2SP.modelCurvature = float(blendful.model_curvature)
|
||||
mdv2sp_send.modelDataV2SP.blendfulGoingAround = bool(blendful.going_around)
|
||||
mdv2sp_send.modelDataV2SP.blendfulLeftProb = blendful.l_prob
|
||||
mdv2sp_send.modelDataV2SP.blendfulRightProb = blendful.r_prob
|
||||
mdv2sp_send.modelDataV2SP.blendfulLaneWidth = float(blendful.lane_width.x)
|
||||
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
||||
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def record_kernel_linear():
|
||||
recorded, hcq_compile, compile_linear = [], realize.hcq_compile, jit.compile_linear
|
||||
|
||||
def recording_compile_linear(*args, **kwargs):
|
||||
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
|
||||
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (linear.src and recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
|
||||
return compile_linear(*args, **kwargs)
|
||||
|
||||
with mock.patch.object(jit, "compile_linear", recording_compile_linear):
|
||||
@@ -38,6 +38,18 @@ def capture_frame(jit, kwargs: dict, output_container: str | None = None):
|
||||
return {"inputs": inputs, "outputs": [tensor.numpy().copy() for tensor in outputs], "output_container": output_container}, result
|
||||
|
||||
|
||||
def kernel_signature(ast, elf) -> list:
|
||||
from tinygrad.dtype import AddrSpace
|
||||
if hasattr(ast, "kernel_params"):
|
||||
if any(p.arg.addrspace is AddrSpace.ALU for p in ast.kernel_params):
|
||||
raise ValueError(f"{elf.name} takes a variable, which plain artifacts don't support")
|
||||
compact = {slot: i for i, slot in enumerate(ast.arg.globals)}
|
||||
return [[p.arg.name, compact[p.arg.slot], repr(p.dtype), [int(x) for x in p._shape]] for p in ast.kernel_params]
|
||||
if not all(isinstance(slot, int) for _, slot, _, _ in elf.signature):
|
||||
raise ValueError("TinyELF.signature lost its slots and this tinygrad has no UOp.kernel_params: update kernel_signature")
|
||||
return [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature]
|
||||
|
||||
|
||||
def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict, memoryview]:
|
||||
from tinygrad.uop.ops import Ops
|
||||
sink = tempfile.TemporaryFile(dir=".")
|
||||
@@ -51,6 +63,12 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
||||
if uop.op is Ops.PARAM:
|
||||
input_specs.setdefault(uop.arg.slot, {"dtype": repr(uop.dtype), "numel": int(uop.max_numel()), "device": uop.device})
|
||||
return {"input": uop.arg.slot}
|
||||
root, offset = uop, 0
|
||||
while root.op in (Ops.AFTER, Ops.BITCAST, Ops.SHRINK):
|
||||
offset, root = offset + (root.src[1].arg * root.src[0].dtype.itemsize if root.op is Ops.SHRINK else 0), root.src[0]
|
||||
if root.op is Ops.ALLOC:
|
||||
base = bases.setdefault(id(root), (len(bases), root, root.dtype))
|
||||
return {"buffer": base[0], "offset": offset, "nbytes": int(uop.max_numel()) * uop.dtype.itemsize, "dtype": repr(uop.dtype)}
|
||||
buffer = uop.buffer
|
||||
root = buffer._base if buffer._base is not None else buffer
|
||||
base = bases.setdefault(id(root), (len(bases), root, uop.base.dtype if buffer._base is None else uop.dtype))
|
||||
@@ -63,7 +81,7 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
||||
elf, info = ast.to_elf(), ast.arg
|
||||
global_size, local_size = info.launch_dims({})
|
||||
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": dataclasses.asdict(elf.target),
|
||||
"signature": [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature],
|
||||
"signature": kernel_signature(ast, elf),
|
||||
"global_size": [int(x) for x in global_size], "local_size": [int(x) for x in local_size],
|
||||
"globals": list(info.globals), "outs": list(info.outs), "ins": list(info.ins), "args": arguments})
|
||||
outs, ins = info.outs, info.ins
|
||||
@@ -77,7 +95,9 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
||||
|
||||
ret = jit.captured.ret
|
||||
returns = {"kind": type(ret).__name__, "tensors": [describe(t.uop.base) | {"shape": [int(x) for x in t.shape]} for t in flatten(ret)]}
|
||||
buffers = [{"nbytes": base.nbytes, "device": base.device, "dtype": repr(dtype)}
|
||||
from tinygrad.device import Buffer
|
||||
assert not [base for index, base, _ in bases.values() if index in needs_init and not isinstance(base, Buffer)], "a kernel reads a scratch before writing it"
|
||||
buffers = [{"nbytes": base.nbytes if isinstance(base, Buffer) else int(base.max_numel()) * dtype.itemsize, "device": base.device, "dtype": repr(dtype)}
|
||||
| ({"init": blob.add(base.as_memoryview())} if index in needs_init else {}) for index, base, dtype in bases.values()]
|
||||
names = list(jit.captured.expected_names)
|
||||
if check is not None:
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import numpy as np
|
||||
|
||||
os.environ.setdefault("CHECK_OOB", "0")
|
||||
os.environ.setdefault("GMMU", "0")
|
||||
if sys.platform == "darwin" and os.environ.get("DEV", "").startswith("MOCKUSB"): # the mock's VRAM is a memfd, which macOS lacks: an unlinked file is the same
|
||||
from tinygrad.runtime.autogen import libc
|
||||
|
||||
def _memfd_create(name, flags):
|
||||
fd, path = tempfile.mkstemp()
|
||||
os.unlink(path)
|
||||
return fd
|
||||
libc.memfd_create = _memfd_create
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.modeld_v2 import convert_upstream_pkl
|
||||
from openpilot.sunnypilot.modeld_v2.convert_upstream_pkl import convert
|
||||
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, load_model
|
||||
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame
|
||||
|
||||
IMG, DESIRE, STATE, OUT = (6, 8, 8), (8,), (4, 16), (1, 32)
|
||||
|
||||
|
||||
class TestConvertUpstreamPkl(OpenpilotTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
if not os.environ.get("TINYGRAD_TESTS"):
|
||||
raise unittest.SkipTest("TINYGRAD_TESTS not set. skipping converter test")
|
||||
from tinygrad import Device, Tensor
|
||||
assert Device.DEFAULT == "AMD" and getattr(Device["AMD"], "is_usb", False), f"{Device.DEFAULT} is not an AMD device behind the USB bridge"
|
||||
assert (Tensor([1, 2, 3]) * 2).tolist() == [2, 4, 6]
|
||||
cls.tmp = tempfile.TemporaryDirectory()
|
||||
cls.rng = np.random.default_rng(0)
|
||||
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
|
||||
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
|
||||
cls.input_specs = {"input_imgs": (IMG, "uint8", Device.DEFAULT), "desire": (DESIRE, "float32", Device.DEFAULT),
|
||||
"features": (STATE, "float32", Device.DEFAULT)}
|
||||
cls.output_specs = {"outputs": (OUT, "float32", Device.DEFAULT), "next_features": (STATE, "float32", Device.DEFAULT)}
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.tmp.cleanup()
|
||||
super().tearDownClass()
|
||||
|
||||
def model(self, input_imgs, desire, features, output_buffers):
|
||||
from tinygrad import Tensor
|
||||
feat = features.mean(axis=0).reshape(1, 16) + input_imgs.float().mean() + desire.sum()
|
||||
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
|
||||
output_buffers["next_features"].assign(features[1:].cat(feat * 2)))
|
||||
|
||||
def inputs(self, seed: int) -> dict:
|
||||
from examples.openpilot.helpers import allocate_inputs
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
def initialize(arrays: dict):
|
||||
for value in arrays.values():
|
||||
value[...] = rng.standard_normal(value.shape) if value.dtype.kind == "f" else rng.integers(0, 256, value.shape)
|
||||
return allocate_inputs(self.input_specs, initialize) | {"output_buffers": allocate_inputs(self.output_specs)}
|
||||
|
||||
def jit(self):
|
||||
from tinygrad import TinyJit
|
||||
run = TinyJit(self.model, prune=True)
|
||||
kwargs = self.inputs(1)
|
||||
run(**kwargs)
|
||||
run(**kwargs)
|
||||
return run
|
||||
|
||||
def assert_matches_eager(self, plain, seed: int):
|
||||
fresh, expected = self.inputs(seed), self.inputs(seed)
|
||||
plain(**fresh)
|
||||
self.model(**expected)
|
||||
for name in self.output_specs:
|
||||
assert np.array_equal(fresh["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
|
||||
|
||||
def convert(self, jit) -> tuple[dict, memoryview]:
|
||||
from tinygrad import Device
|
||||
kwargs = self.inputs(2)
|
||||
frame, _ = capture_frame(jit, kwargs, "output_buffers")
|
||||
header, blob = convert(jit, kwargs, frame, Device[Device.DEFAULT].renderer.target)
|
||||
kernels = jit.captured._linear.src[0].without_after.arg.aux.kernels
|
||||
assert [call["name"] for call in header["calls"]] == [re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]) for kernel in kernels]
|
||||
assert all(re.fullmatch(r"\w+", call["name"]) for call in header["calls"]), "kernel names: failure point 1"
|
||||
assert [(tuple(call["outs"]), tuple(call["ins"])) for call in header["calls"]] == [tuple(kernel[6]) for kernel in kernels], "outs/ins: failure point 1"
|
||||
bound = {argument["input"] for call in header["calls"] for argument in call["args"] if "input" in argument}
|
||||
assert bound == {spec["slot"] for spec in header["inputs"]}, f"input slots bound by pokes {sorted(bound)}: failure point 4"
|
||||
assert [spec["name"] for spec in header["inputs"]][:len(self.input_specs)] == sorted(self.input_specs)
|
||||
return header, blob
|
||||
|
||||
def test_converted_jit_matches_eager(self): # the submit decoded into plain calls, replayed against its own frame
|
||||
jit = self.jit()
|
||||
plain = PlainJit(*self.convert(jit))
|
||||
self.assert_matches_eager(plain, 3)
|
||||
self.assert_matches_eager(plain, 4)
|
||||
|
||||
def test_pickle_round_trip_through_main(self):
|
||||
from examples.openpilot.helpers import dump_pickle, load_pickle
|
||||
from tinygrad.engine.realize import lower_and_compile
|
||||
pkl, out = os.path.join(self.tmp.name, "tiny_tinygrad.pkl"), os.path.join(self.tmp.name, "tiny_plain.pkl")
|
||||
dump_pickle({"run": self.jit(), "input_specs": self.input_specs, "output_specs": self.output_specs, "metadata": {"name": "tiny"}}, pkl)
|
||||
|
||||
artifact = load_pickle(pkl, out_of_band=True)
|
||||
artifact["run"].captured._linear = lower_and_compile(artifact["run"].captured._linear)
|
||||
self.assert_matches_eager(PlainJit(*self.convert(artifact["run"])), 5)
|
||||
|
||||
with mock.patch.object(sys, "argv", ["convert_upstream_pkl", pkl, out]):
|
||||
convert_upstream_pkl.main()
|
||||
bundle = load_model(out, cache=False, verify=True)
|
||||
assert bundle["metadata"] == {"name": "tiny"} and bundle["input_specs"] == self.input_specs and bundle["output_specs"] == self.output_specs
|
||||
self.assert_matches_eager(bundle["run"], 6)
|
||||
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
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 dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
import numpy as np
|
||||
import unittest
|
||||
|
||||
import openpilot.sunnypilot.modeld_v2.helpers as helpers
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.modeld_v2 import compile_warp
|
||||
from openpilot.sunnypilot.modeld_v2.frame_fusion import frame_part, pack_layout
|
||||
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, cache_is_fresh, load_model, read_parts, write_bundle
|
||||
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame, record_kernel_linear, serialize_jit
|
||||
|
||||
CAM_W, CAM_H, MODEL_W, MODEL_H = 32, 16, 16, 8
|
||||
FRAME = f"{CAM_W},{CAM_H},{CAM_W},{CAM_H},{CAM_H // 2},{CAM_W * CAM_H * 3 // 2}"
|
||||
FRAME_SIZE = CAM_W * CAM_H * 3 // 2
|
||||
HOST = {"desire": (8,), "traffic_convention": (2,)}
|
||||
STATE, OUT = (4, 16), (1, 32)
|
||||
|
||||
|
||||
def compile_jit(fn, inputs: dict, output_container: str | None = None) -> tuple[dict, memoryview]:
|
||||
from tinygrad import TinyJit
|
||||
run = TinyJit(fn, prune=True)
|
||||
with record_kernel_linear() as recorded:
|
||||
run(**inputs)
|
||||
run(**inputs)
|
||||
check, _ = capture_frame(run, inputs, output_container)
|
||||
assert len(recorded) == 1, f"expected one captured kernel list, got {len(recorded)}"
|
||||
return serialize_jit(run, recorded[0], check)
|
||||
|
||||
|
||||
class TestPlainArtifact(OpenpilotTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
if not os.environ.get("TINYGRAD_TESTS"):
|
||||
raise unittest.SkipTest("TINYGRAD_TESTS env var not set, skipping artifact test.")
|
||||
from tinygrad import Tensor
|
||||
cls.tmp = tempfile.TemporaryDirectory()
|
||||
cls.rng = np.random.default_rng(0)
|
||||
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
|
||||
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.tmp.cleanup()
|
||||
super().tearDownClass()
|
||||
|
||||
def model(self, x, h): # a two input, one weight model: the shape of a policy without the size
|
||||
return ((x @ self.weights + self.bias).relu() + h.mean()).contiguous()
|
||||
|
||||
def tensors(self, seed: int):
|
||||
from tinygrad import Tensor
|
||||
rng = np.random.default_rng(seed)
|
||||
return {"x": Tensor(rng.standard_normal((1, 16)).astype(np.float32)).realize(), "h": Tensor(rng.standard_normal((4, 16)).astype(np.float32)).realize()}
|
||||
|
||||
def artifact(self, name: str = "model") -> str:
|
||||
path = os.path.join(self.tmp.name, f"{name}_tinygrad.pkl")
|
||||
if not os.path.exists(path):
|
||||
write_bundle(path, {"run": compile_jit(self.model, self.tensors(1))}, {"metadata": {"name": name}})
|
||||
return path
|
||||
|
||||
def test_round_trip_matches_jit(self): # compiler -> bundle -> PlainJit, replay verified, then fresh inputs bit exact with eager tinygrad
|
||||
bundle = load_model(self.artifact(), cache=False, verify=True)
|
||||
assert bundle["metadata"] == {"name": "model"} and isinstance(bundle["run"], PlainJit)
|
||||
fresh = self.tensors(2)
|
||||
assert np.array_equal(bundle["run"](**fresh).numpy(), self.model(**fresh).numpy())
|
||||
|
||||
def test_replay_check_catches_a_changed_kernel(self):
|
||||
parts, extras = read_parts(self.artifact())
|
||||
header, blob = parts["run"]
|
||||
poisoned = dict(header, check=dict(header["check"], outputs=[header["check"]["inputs"][0][1]]))
|
||||
with self.assertRaises(helpers.ArtifactMismatch):
|
||||
PlainJit(poisoned, blob)
|
||||
|
||||
def test_cache_round_trip_holds_no_weights_and_is_rebuilt_when_stale(self):
|
||||
path = self.artifact("cached")
|
||||
expected = load_model(path, cache=False, verify=False)["run"](**self.tensors(3)).numpy()
|
||||
load_model(path)
|
||||
assert cache_is_fresh(path)
|
||||
with open(f"{path}.cache", "rb") as file:
|
||||
cache = file.read()
|
||||
assert self.weights.numpy().tobytes() not in cache and len(cache) < os.path.getsize(path)
|
||||
with mock.patch.object(helpers, "PlainJit", side_effect=AssertionError("a fresh cache must not rebuild")):
|
||||
cached = load_model(path)
|
||||
assert np.array_equal(cached["run"](**self.tensors(3)).numpy(), expected)
|
||||
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
|
||||
assert not cache_is_fresh(path)
|
||||
assert isinstance(load_model(path)["run"], PlainJit)
|
||||
assert cache_is_fresh(path)
|
||||
assert not cache_is_fresh(path) # keyed on the tinygrad that wrote it, so the next boot after a bump rebuilds once
|
||||
os.utime(path)
|
||||
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
|
||||
assert not cache_is_fresh(path)
|
||||
|
||||
def test_fused_with_warp_matches_warp_then_model(self):
|
||||
from tinygrad import Tensor
|
||||
warp_path = os.path.join(self.tmp.name, "warp_tinygrad.pkl")
|
||||
with mock.patch.object(sys, "argv", ["compile_warp", "--frame", FRAME, "--warp-to", f"{MODEL_W}x{MODEL_H}", "--output", warp_path]):
|
||||
compile_warp.main()
|
||||
warp = load_model(warp_path, cache=False)["run"]
|
||||
|
||||
def native(new_img, desire, traffic_convention, state_feat_q, output_buffers):
|
||||
feat = state_feat_q.mean(axis=0).reshape(1, 16) + new_img.float().mean() + desire.sum() + traffic_convention[0]
|
||||
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
|
||||
output_buffers["next_state_feat_q"].assign(state_feat_q[1:].cat(feat * 2)))
|
||||
|
||||
def model_inputs(new_img):
|
||||
return {"new_img": new_img} | {name: Tensor(np.ones(shape, np.float32) * 0.5).realize() for name, shape in HOST.items()} | \
|
||||
{"state_feat_q": Tensor(self.rng.standard_normal(STATE).astype(np.float32)).realize(),
|
||||
"output_buffers": {"outputs": Tensor.zeros(OUT).contiguous().realize(), "next_state_feat_q": Tensor.zeros(STATE).contiguous().realize()}}
|
||||
frames = Tensor(self.rng.integers(0, 256, (2, FRAME_SIZE), dtype=np.uint8)).realize()
|
||||
tfm = Tensor(np.tile(np.eye(3, dtype=np.float32), (2, 1, 1))).realize()
|
||||
path = os.path.join(self.tmp.name, "native_tinygrad.pkl")
|
||||
part = compile_jit(native, model_inputs(warp(input_frame=frames, M_inv=tfm).realize()), "output_buffers")
|
||||
extras = {"input_specs": {"new_img": ((2, 6, MODEL_H // 2, MODEL_W // 2), "|u1", "CPU"), "state_feat_q": (STATE, "<f4", "CPU")}
|
||||
| {name: (shape, "<f4", "CPU") for name, shape in HOST.items()},
|
||||
"output_specs": {"outputs": (OUT, "<f4", "CPU"), "next_state_feat_q": (STATE, "<f4", "CPU")},
|
||||
"metadata": {"output_shapes": {"outputs": OUT, "next_state_feat_q": STATE}}}
|
||||
write_bundle(path, {"run": part}, extras)
|
||||
|
||||
fused = load_model(path, cache=False, extend=functools.partial(frame_part, warp_path))
|
||||
assert fused["run"].input_names == ["host", "state_feat_q"]
|
||||
offsets, packed = pack_layout(HOST, FRAME_SIZE)
|
||||
host = np.zeros(packed, np.uint8)
|
||||
inputs = model_inputs(None)
|
||||
for name, shape in ({"tfm": (2, 3, 3)} | HOST).items():
|
||||
np.ndarray(shape, np.float32, host, offsets[name])[...] = tfm.numpy() if name == "tfm" else inputs[name].numpy()
|
||||
host[offsets["frames"]:] = frames.numpy().ravel()
|
||||
fused["run"](host=Tensor(host, device="NPY"), state_feat_q=inputs["state_feat_q"], output_buffers=inputs["output_buffers"])
|
||||
|
||||
expected = model_inputs(warp(input_frame=frames, M_inv=tfm).realize()) | {k: inputs[k] for k in ("state_feat_q",)}
|
||||
native(**expected)
|
||||
for name in ("outputs", "next_state_feat_q"):
|
||||
assert np.array_equal(inputs["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
|
||||
|
||||
def test_tinygrad_api_the_loader_reads(self):
|
||||
import tinygrad.engine.jit as jit
|
||||
from tinygrad.device import Buffer, TinyELF
|
||||
from tinygrad.helpers import Target
|
||||
from tinygrad.uop.ops import ParamArg, ProgramInfo, UOp
|
||||
fields = {cls: {f.name for f in dataclasses.fields(cls)} for cls in (ProgramInfo, ParamArg, Target)}
|
||||
assert fields[ProgramInfo] >= {"global_size", "local_size", "globals", "outs", "ins", "target"}, "ProgramInfo changed: update helpers._program_uop"
|
||||
assert fields[ParamArg] >= {"slot", "dtype", "name", "image"}, "ParamArg changed: update helpers._program_uop"
|
||||
assert fields[Target] >= {"device", "renderer", "arch"}, "Target changed: update the 'target' key in plain_artifact_compiler.serialize_jit"
|
||||
assert "signature" in getattr(TinyELF, "__annotations__", {}) or hasattr(UOp, "kernel_params"), \
|
||||
"kernel signatures moved: update plain_artifact_compiler.serialize_jit"
|
||||
assert callable(getattr(UOp, "param", None)) and callable(getattr(UOp, "from_buffer", None)), "UOp.param/from_buffer: update helpers.PlainJit"
|
||||
assert callable(getattr(UOp, "store_call", None)) and callable(getattr(UOp, "call", None)), "UOp.call/store_call: update helpers.PlainJit"
|
||||
assert callable(getattr(Buffer, "view", None)) and callable(getattr(Buffer, "ensure_allocated", None)), "Buffer.view: update helpers._upload"
|
||||
assert callable(getattr(jit, "compile_linear", None)) and callable(getattr(jit, "_prepare_jit_inputs", None)), "engine.jit: update helpers._Jit"
|
||||
assert "expected_names" in inspect.signature(jit.CapturedJit).parameters, "CapturedJit: update helpers.PlainJit and plain_artifact_compiler.serialize_jit"
|
||||
@@ -22,7 +22,7 @@ DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "mode
|
||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
|
||||
BIG_MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "big_model_hash")
|
||||
SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx")
|
||||
BIG_SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "big_driving_supercombo.onnx")
|
||||
BIG_DRIVING_PKL_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "big_driving_tinygrad.pkl")
|
||||
|
||||
|
||||
def _read_model_name_fields():
|
||||
@@ -47,18 +47,17 @@ def update_model_hash():
|
||||
|
||||
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
|
||||
|
||||
if os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
|
||||
import subprocess
|
||||
rel = os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd())
|
||||
pointer = subprocess.check_output(["git", "show", f"HEAD:{rel}"], text=True)
|
||||
oid = next(l.split(":", 1)[1] for l in pointer.splitlines() if l.startswith("oid sha256:"))
|
||||
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
|
||||
big_combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
|
||||
import subprocess
|
||||
rel = os.path.relpath(BIG_DRIVING_PKL_PATH, os.getcwd())
|
||||
pointer = subprocess.check_output(["git", "show", f"HEAD:{rel}"], text=True)
|
||||
oid = next(l.split(":", 1)[1] for l in pointer.splitlines() if l.startswith("oid sha256:"))
|
||||
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
|
||||
big_combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
|
||||
|
||||
with open(BIG_MODEL_HASH_PATH, "w") as f:
|
||||
f.write(big_combined_hash)
|
||||
with open(BIG_MODEL_HASH_PATH, "w") as f:
|
||||
f.write(big_combined_hash)
|
||||
|
||||
print(f"Generated and updated new big model hash to {BIG_MODEL_HASH_PATH}")
|
||||
print(f"Generated and updated new big model hash to {BIG_MODEL_HASH_PATH}")
|
||||
|
||||
|
||||
def get_ref_for_name(url: str, name: str) -> str:
|
||||
|
||||
@@ -7,7 +7,10 @@ See the LICENSE.md file in the root directory for more details.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import requests
|
||||
from openpilot.common.params import Params
|
||||
@@ -22,12 +25,101 @@ from openpilot.sunnypilot.models.helpers import (ACTIVE_BUNDLE_KEYS, get_active_
|
||||
|
||||
# (connect, read) seconds. read is per-request inactivity, not a total cap
|
||||
DOWNLOAD_TIMEOUT = (30, 30)
|
||||
# HuggingFace throttles each connection to ~1-2 MB/s but not the connection count; 12 saturates the device link
|
||||
MAX_CONCURRENT_PIECES = 12
|
||||
PIECE_SIZE = 8 * 1024 * 1024
|
||||
PIECE_ATTEMPTS = 3
|
||||
PIECE_BACKOFF = 1.0 # seconds, growing linearly: waits 1 s then 2 s between a piece's three attempts
|
||||
TRANSIENT_HTTP_STATUS = frozenset({408, 429, 500, 502, 503, 504})
|
||||
REPORT_INTERVAL = 0.5 # seconds between progress publications
|
||||
# byte ranges count encoded bytes but iter_content yields decoded ones, so ask for an unencoded body
|
||||
NO_COMPRESSION = {"Accept-Encoding": "identity"}
|
||||
SPEED_SMOOTHING = 0.7 # weight of the previous speed sample in the published speed
|
||||
SPEED_FLOOR = 0.05 * 1024 ** 2 # bytes/s; anything slower would only display as 0.0 MB/s, so it reads as no rate
|
||||
|
||||
|
||||
class DownloadCancelled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _is_transient(e: BaseException) -> bool:
|
||||
if isinstance(e, requests.HTTPError):
|
||||
return e.response is not None and e.response.status_code in TRANSIENT_HTTP_STATUS
|
||||
return isinstance(e, (requests.ConnectionError, requests.Timeout, requests.exceptions.ChunkedEncodingError))
|
||||
|
||||
|
||||
class _Progress:
|
||||
def __init__(self):
|
||||
self._bytes = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def add_bytes(self, n: int) -> None:
|
||||
with self._lock:
|
||||
self._bytes += n
|
||||
|
||||
def snapshot(self) -> int:
|
||||
with self._lock:
|
||||
return self._bytes
|
||||
|
||||
|
||||
def _piece_ranges(total: int, piece_size: int) -> list[tuple[int, int]]:
|
||||
return [(start, min(start + piece_size, total)) for start in range(0, total, piece_size)]
|
||||
|
||||
|
||||
def _prepare_target(path: str, total: int) -> None:
|
||||
with open(path, "wb") as f:
|
||||
f.truncate(total)
|
||||
|
||||
|
||||
def _content_range_total(response: requests.Response) -> int | None:
|
||||
if response.status_code != 206:
|
||||
return None
|
||||
match = re.fullmatch(r"bytes \d+-\d+/(\d+)", response.headers.get("Content-Range", ""))
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def _probe_size(url: str) -> tuple[int, bool]:
|
||||
with requests.get(url, headers={**NO_COMPRESSION, "Range": "bytes=0-0"}, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
|
||||
response.raise_for_status()
|
||||
total = _content_range_total(response)
|
||||
if total is not None:
|
||||
return total, True
|
||||
return int(response.headers.get("Content-Length", 0)), False
|
||||
|
||||
|
||||
def _fetch_piece(url: str, path: str, index: int, start: int, end: int | None, progress: _Progress,
|
||||
cancel: threading.Event, block_size: int) -> None:
|
||||
headers = {**NO_COMPRESSION, "Range": f"bytes={start}-{end - 1}"} if end is not None else NO_COMPRESSION
|
||||
label = f"{os.path.basename(path)} piece {index}"
|
||||
for attempt in range(PIECE_ATTEMPTS):
|
||||
written = 0
|
||||
try:
|
||||
with requests.get(url, headers=headers, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
|
||||
response.raise_for_status()
|
||||
if end is not None and response.status_code != 206:
|
||||
raise ValueError(f"server ignored byte range for {label}")
|
||||
with open(path, "r+b") as f:
|
||||
f.seek(start)
|
||||
for block in response.iter_content(chunk_size=block_size):
|
||||
if cancel.is_set():
|
||||
raise DownloadCancelled("Download cancelled")
|
||||
f.write(block)
|
||||
written += len(block)
|
||||
progress.add_bytes(len(block))
|
||||
if end is None:
|
||||
f.truncate() # unknown length: the body defines the file size
|
||||
if end is not None and written != end - start:
|
||||
raise requests.exceptions.ConnectionError(f"short read: {written} of {end - start} bytes")
|
||||
return
|
||||
except requests.RequestException as e:
|
||||
progress.add_bytes(-written) # the piece restarts from scratch
|
||||
if not _is_transient(e) or attempt == PIECE_ATTEMPTS - 1:
|
||||
raise
|
||||
cloudlog.warning(f"retrying {label} after {type(e).__name__}: {e}")
|
||||
if cancel.wait(PIECE_BACKOFF * (1 + attempt)):
|
||||
raise DownloadCancelled("Download cancelled") from None
|
||||
|
||||
|
||||
class ModelManagerSP:
|
||||
"""Manages model downloads and status reporting"""
|
||||
|
||||
@@ -41,7 +133,7 @@ class ModelManagerSP:
|
||||
self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {}
|
||||
self.selected_bundle: custom.ModelManagerSP.ModelBundle = None
|
||||
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
|
||||
self._chunk_size = 128 * 1000 # 128 KB chunks
|
||||
self._block_size = 128 * 1000 # 128 KB network read blocks
|
||||
self._download_start_times: dict[str, float] = {} # Track start time per model
|
||||
self._download_ref: bytes | str | None = None
|
||||
|
||||
@@ -56,7 +148,6 @@ class ModelManagerSP:
|
||||
self._download_ref = None
|
||||
|
||||
def _sync_artifact_progress(self, source_artifact) -> None:
|
||||
"""Mirror download progress to all artifacts sharing the same filename in the selected bundle."""
|
||||
if not self.selected_bundle:
|
||||
return
|
||||
for model in self.selected_bundle.models:
|
||||
@@ -65,9 +156,9 @@ class ModelManagerSP:
|
||||
artifact.downloadProgress.status = source_artifact.downloadProgress.status
|
||||
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
|
||||
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
|
||||
artifact.downloadProgress.speed = source_artifact.downloadProgress.speed
|
||||
|
||||
def _calculate_eta(self, filename: str, progress: float) -> int:
|
||||
"""Calculate ETA based on elapsed time and current progress"""
|
||||
if filename not in self._download_start_times or progress <= 0:
|
||||
return 60 # Default ETA for new downloads
|
||||
|
||||
@@ -75,39 +166,74 @@ class ModelManagerSP:
|
||||
if elapsed_time <= 0:
|
||||
return 60
|
||||
|
||||
# If we're at X% after Y seconds, we can estimate total time as (Y / X) * 100
|
||||
total_estimated_time = (elapsed_time / progress) * 100
|
||||
eta = total_estimated_time - elapsed_time
|
||||
|
||||
return max(1, int(eta)) # Return at least 1 second if download is ongoing
|
||||
|
||||
async def _download_file(self, url: str, path: str, model) -> None:
|
||||
"""Downloads a file with progress tracking"""
|
||||
self._download_start_times[model.fileName] = time.monotonic()
|
||||
def _set_progress(self, artifact, status, progress: float, eta: int = 0, speed: float = 0.0) -> None:
|
||||
artifact.downloadProgress.status = status
|
||||
artifact.downloadProgress.progress = progress
|
||||
artifact.downloadProgress.eta = eta
|
||||
artifact.downloadProgress.speed = speed
|
||||
self._sync_artifact_progress(artifact)
|
||||
self._report_status()
|
||||
|
||||
with requests.get(url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response: # noqa: ASYNC210
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
bytes_downloaded = 0
|
||||
def _publish_progress(self, artifact, done_bytes: int, total: int, speed: float) -> None:
|
||||
# held at 99 until the file passes its hash check
|
||||
progress = min(99.0, done_bytes / total * 100) if total > 0 else 0.0
|
||||
if speed > 0 and total > 0:
|
||||
eta = max(1, int((total - done_bytes) / speed))
|
||||
else:
|
||||
eta = self._calculate_eta(artifact.fileName, progress)
|
||||
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, progress, eta, speed)
|
||||
|
||||
with open(path, 'wb') as f: # noqa: ASYNC230
|
||||
for chunk in response.iter_content(chunk_size=self._chunk_size): # type: bytes
|
||||
f.write(chunk)
|
||||
bytes_downloaded += len(chunk)
|
||||
async def _report_until_done(self, tasks: list[asyncio.Future], artifact, progress: _Progress, total: int,
|
||||
started: tuple[int, float]) -> None:
|
||||
pending = set(tasks)
|
||||
last_bytes, last_time = started
|
||||
speed = 0.0
|
||||
while pending:
|
||||
done, pending = await asyncio.wait(pending, timeout=REPORT_INTERVAL)
|
||||
for task in done:
|
||||
task.result() # re-raises a worker failure
|
||||
if self._download_interrupted():
|
||||
raise DownloadCancelled("Download cancelled")
|
||||
now = time.monotonic()
|
||||
done_bytes = progress.snapshot()
|
||||
if (dt := now - last_time) > 0:
|
||||
instant = max(0.0, (done_bytes - last_bytes) / dt) # bytes/s this tick; clamped since retried pieces can lower the byte count
|
||||
speed = instant if speed == 0 else SPEED_SMOOTHING * speed + (1 - SPEED_SMOOTHING) * instant
|
||||
if speed < SPEED_FLOOR: # a stall decays toward 0 without reaching it, and a near-0 speed overflows the eta
|
||||
speed = 0.0
|
||||
last_time, last_bytes = now, done_bytes
|
||||
self._publish_progress(artifact, done_bytes, total, speed)
|
||||
|
||||
if self._download_interrupted():
|
||||
raise DownloadCancelled("Download cancelled")
|
||||
async def _download_file(self, url: str, path: str, artifact) -> None:
|
||||
self._download_start_times[artifact.fileName] = time.monotonic()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
if total_size > 0:
|
||||
progress = (bytes_downloaded / total_size) * 100
|
||||
model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
|
||||
model.downloadProgress.progress = progress
|
||||
model.downloadProgress.eta = self._calculate_eta(model.fileName, progress)
|
||||
self._sync_artifact_progress(model)
|
||||
self._report_status()
|
||||
total, ranged = await loop.run_in_executor(None, _probe_size, url)
|
||||
pieces: list[tuple[int, int | None]] = list(_piece_ranges(total, PIECE_SIZE)) if ranged else [(0, None)]
|
||||
await loop.run_in_executor(None, _prepare_target, path, total)
|
||||
progress = _Progress()
|
||||
cancel = threading.Event()
|
||||
started = progress.snapshot(), time.monotonic() # before any worker can land bytes
|
||||
|
||||
# Clean up start time after download completes
|
||||
del self._download_start_times[model.fileName]
|
||||
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_PIECES) as pool:
|
||||
tasks = [loop.run_in_executor(pool, _fetch_piece, url, path, i, start, end, progress, cancel, self._block_size)
|
||||
for i, (start, end) in enumerate(pieces)]
|
||||
try:
|
||||
await self._report_until_done(tasks, artifact, progress, total, started)
|
||||
except BaseException as e:
|
||||
cancel.set()
|
||||
if isinstance(e, DownloadCancelled):
|
||||
artifact.downloadProgress.canceling = True
|
||||
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, artifact.downloadProgress.progress)
|
||||
for task in tasks:
|
||||
task.cancel() # drops queued pieces; running ones see the event at their next block
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
raise
|
||||
del self._download_start_times[artifact.fileName]
|
||||
|
||||
async def _process_artifact(self, artifact, destination_path: str) -> None:
|
||||
if not artifact.downloadUri.uri:
|
||||
@@ -119,33 +245,31 @@ class ModelManagerSP:
|
||||
expected_hash = artifact.downloadUri.sha256
|
||||
filename = artifact.fileName
|
||||
full_path = os.path.join(destination_path, filename)
|
||||
status = custom.ModelManagerSP.DownloadStatus
|
||||
artifact.downloadProgress.canceling = False
|
||||
|
||||
try:
|
||||
self._set_progress(artifact, status.verifying, 0)
|
||||
if await verify_file(full_path, expected_hash):
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
||||
artifact.downloadProgress.progress = 100
|
||||
artifact.downloadProgress.eta = 0
|
||||
self._sync_artifact_progress(artifact)
|
||||
self._report_status()
|
||||
self._set_progress(artifact, status.cached, 100)
|
||||
return
|
||||
|
||||
self._set_progress(artifact, status.downloading, 0)
|
||||
await self._download_file(url, full_path, artifact)
|
||||
self._set_progress(artifact, status.verifying, 99)
|
||||
if not await verify_file(full_path, expected_hash):
|
||||
raise ValueError(f"Hash validation failed for {filename}")
|
||||
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
|
||||
artifact.downloadProgress.progress = 100
|
||||
artifact.downloadProgress.eta = 0
|
||||
self._sync_artifact_progress(artifact)
|
||||
self._report_status()
|
||||
self._set_progress(artifact, status.downloaded, 100)
|
||||
|
||||
except DownloadCancelled:
|
||||
self._download_start_times.pop(artifact.fileName, None)
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
|
||||
artifact.downloadProgress.status = status.failed
|
||||
artifact.downloadProgress.eta = 0
|
||||
artifact.downloadProgress.speed = 0
|
||||
self._sync_artifact_progress(artifact)
|
||||
if self.selected_bundle:
|
||||
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
|
||||
self.selected_bundle.status = status.failed
|
||||
self._report_status()
|
||||
raise
|
||||
|
||||
@@ -153,11 +277,12 @@ class ModelManagerSP:
|
||||
cloudlog.error(f"Error downloading {filename}: {str(e)}")
|
||||
if os.path.isfile(full_path): # noqa: ASYNC240
|
||||
os.remove(full_path)
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
|
||||
artifact.downloadProgress.status = status.failed
|
||||
artifact.downloadProgress.eta = 0
|
||||
artifact.downloadProgress.speed = 0
|
||||
self._sync_artifact_progress(artifact)
|
||||
if self.selected_bundle:
|
||||
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
|
||||
self.selected_bundle.status = status.failed
|
||||
self._report_status()
|
||||
self._download_start_times.pop(artifact.fileName, None)
|
||||
raise
|
||||
@@ -197,6 +322,7 @@ class ModelManagerSP:
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
||||
artifact.downloadProgress.progress = 100
|
||||
artifact.downloadProgress.eta = 0
|
||||
artifact.downloadProgress.speed = 0
|
||||
else:
|
||||
seen_artifacts.add(artifact.fileName)
|
||||
await self._process_artifact(artifact, destination_path)
|
||||
@@ -272,12 +398,7 @@ class ModelManagerSP:
|
||||
rk.keep_time()
|
||||
|
||||
def clear_model_cache(self) -> None:
|
||||
"""
|
||||
Clears the model cache directory of all files except those in the active model bundle.
|
||||
"""
|
||||
|
||||
# Get list of files used by both slots' selected bundles (either may become
|
||||
# the truly active bundle depending on hardware availability)
|
||||
""" Clears the model cache directory of all files except those in the active model bundle."""
|
||||
active_files = []
|
||||
for source in ACTIVE_BUNDLE_KEYS:
|
||||
if selected_bundle := get_selected_bundle(self.params, source):
|
||||
@@ -285,7 +406,6 @@ class ModelManagerSP:
|
||||
if model.artifact.fileName:
|
||||
active_files.append(model.artifact.fileName)
|
||||
|
||||
# Remove all files except active ones
|
||||
model_dir = Paths.model_root()
|
||||
try:
|
||||
for filename in os.listdir(model_dir):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
DEFAULT_MODEL = "CD210"
|
||||
DEFAULT_MODEL_REF = "5b6436a90cf6902b8aaa71c2b6f3d7164d8ae391"
|
||||
DEFAULT_BIG_MODEL = "Cinque Terre Model v2"
|
||||
DEFAULT_BIG_MODEL_REF = "37bfa1413edcdc2e8844984b83727c33f81d8f46"
|
||||
DEFAULT_BIG_MODEL = "Mountain Dew Model v3"
|
||||
DEFAULT_BIG_MODEL_REF = "1e219d7785cfb7b7343c7f2366347fc86e7ce5f2"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2c814f08a2c51323b87839fbf8d2c2a9853a2b5536271b3d67f7b7a2de7f9374
|
||||
330fdf61b5519642ebccd93aa50f2d7c4ba6c08c5a10c36a2cf155e498700cef
|
||||
@@ -10,7 +10,7 @@ import subprocess
|
||||
|
||||
from openpilot.sunnypilot import get_file_hash
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH, BIG_MODEL_HASH_PATH, \
|
||||
BIG_SUPERCOMBO_ONNX_PATH, _read_model_name_fields
|
||||
BIG_DRIVING_PKL_PATH, _read_model_name_fields
|
||||
import hashlib
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
|
||||
@@ -36,12 +36,9 @@ class TestDefaultModel(OpenpilotTestCase):
|
||||
|
||||
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
|
||||
|
||||
def test_compare_big_onnx_hashes(self):
|
||||
if not os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
|
||||
self.skipTest("big_driving_supercombo.onnx not present")
|
||||
|
||||
def test_compare_big_pkl_hashes(self):
|
||||
fields = _read_model_name_fields()
|
||||
oid = _get_lfs_oid(os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd()))
|
||||
oid = _get_lfs_oid(os.path.relpath(BIG_DRIVING_PKL_PATH, os.getcwd()))
|
||||
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
|
||||
combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.mapd import MAPD_AVAILABLE
|
||||
from openpilot.sunnypilot.navd.helpers import Coordinate
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V
|
||||
|
||||
@@ -76,7 +75,7 @@ class SmartCruiseControlMap:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params
|
||||
self.enabled = MAPD_AVAILABLE and self.params.get_bool("SmartCruiseControlMap")
|
||||
self.enabled = self.params.get_bool("SmartCruiseControlMap")
|
||||
self.long_enabled = False
|
||||
self.long_override = False
|
||||
self.is_enabled = False
|
||||
@@ -102,7 +101,7 @@ class SmartCruiseControlMap:
|
||||
|
||||
def update_params(self):
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
self.enabled = MAPD_AVAILABLE and self.params.get_bool("SmartCruiseControlMap")
|
||||
self.enabled = self.params.get_bool("SmartCruiseControlMap")
|
||||
|
||||
def update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||
gps = sm[self.gps_location_service]
|
||||
|
||||
+1
-14
@@ -8,7 +8,7 @@ import json
|
||||
import math
|
||||
import platform
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.common.params import Params
|
||||
@@ -23,8 +23,6 @@ MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState
|
||||
class TestSmartCruiseControlMap(OpenpilotTestCase):
|
||||
|
||||
def setup_method(self):
|
||||
self.mapd_patch = patch("openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller.MAPD_AVAILABLE", True)
|
||||
self.mapd_patch.start()
|
||||
self.params = Params()
|
||||
self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params
|
||||
self.reset_params()
|
||||
@@ -32,9 +30,6 @@ class TestSmartCruiseControlMap(OpenpilotTestCase):
|
||||
self.sm = MagicMock()
|
||||
self.sm.__getitem__.return_value = SimpleNamespace(latitude=0.0, longitude=0.0)
|
||||
|
||||
def teardown_method(self):
|
||||
self.mapd_patch.stop()
|
||||
|
||||
def reset_params(self):
|
||||
self.params.put_bool("SmartCruiseControlMap", True, block=True)
|
||||
|
||||
@@ -55,14 +50,6 @@ class TestSmartCruiseControlMap(OpenpilotTestCase):
|
||||
assert self.scc_m.state == VisionState.disabled
|
||||
assert not self.scc_m.is_active
|
||||
|
||||
def test_mapd_unavailable(self):
|
||||
self.mapd_patch.stop()
|
||||
self.scc_m = SmartCruiseControlMap()
|
||||
for _ in range(int(10. / DT_MDL)):
|
||||
self.scc_m.update(self.sm, True, False, 0., 0., 0.)
|
||||
assert self.scc_m.state == VisionState.disabled
|
||||
self.mapd_patch.start()
|
||||
|
||||
def test_disabled(self):
|
||||
for _ in range(int(10. / DT_MDL)):
|
||||
self.scc_m.update(self.sm, False, False, 0., 0., 0.)
|
||||
|
||||
@@ -13,7 +13,6 @@ from openpilot.common.gps import get_gps_location_service
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD, get_sanitize_int_param
|
||||
from openpilot.sunnypilot.mapd import MAPD_AVAILABLE
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy, OffsetType
|
||||
|
||||
@@ -49,8 +48,6 @@ class SpeedLimitResolver:
|
||||
Policy.max().value,
|
||||
self.params
|
||||
)
|
||||
if not MAPD_AVAILABLE:
|
||||
self.policy = Policy.car_state_only
|
||||
self._policy_to_sources_map = {
|
||||
Policy.car_state_only: [SpeedLimitSource.car],
|
||||
Policy.map_data_only: [SpeedLimitSource.map],
|
||||
@@ -94,7 +91,7 @@ class SpeedLimitResolver:
|
||||
|
||||
def update_params(self):
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
self.policy = self.params.get("SpeedLimitPolicy", return_default=True) if MAPD_AVAILABLE else Policy.car_state_only
|
||||
self.policy = self.params.get("SpeedLimitPolicy", return_default=True)
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.offset_type = self.params.get("SpeedLimitOffsetType", return_default=True)
|
||||
self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True)
|
||||
|
||||
@@ -15,7 +15,6 @@ from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.sunnypilot.mapd import MAPD_AVAILABLE
|
||||
|
||||
|
||||
# Wire-protocol version for the capabilities payload. Bump on breaking changes
|
||||
@@ -44,7 +43,6 @@ CAPABILITY_FIELDS = (
|
||||
"device_type",
|
||||
"subaru_has_sng",
|
||||
"hyundai_alpha_long_available",
|
||||
"mapd_available",
|
||||
)
|
||||
|
||||
CAPABILITY_LABELS: dict[str, str] = {
|
||||
@@ -67,7 +65,6 @@ CAPABILITY_LABELS: dict[str, str] = {
|
||||
"device_type": "Device type",
|
||||
"subaru_has_sng": "Subaru Stop-and-Go available",
|
||||
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
|
||||
"mapd_available": "Map data available",
|
||||
}
|
||||
|
||||
# Explicit defaults for non-boolean capability fields
|
||||
@@ -135,7 +132,6 @@ def generate_capabilities(params: Params | None = None) -> dict:
|
||||
caps["is_sp_release"] = params.get_bool("IsReleaseSpBranch")
|
||||
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
|
||||
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
|
||||
caps["mapd_available"] = MAPD_AVAILABLE
|
||||
|
||||
bundle = params.get("CarPlatformBundle")
|
||||
bundle_brand = _bundle_field(bundle, "brand")
|
||||
|
||||
@@ -156,7 +156,7 @@ The tables below describe the **compiled** `settings_ui.json` schema — what th
|
||||
|
||||
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
|
||||
|
||||
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`, `mapd_available`
|
||||
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -857,47 +857,19 @@
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "Map Data Only",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "mapd_available",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
"label": "Map Data Only"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": "Car State Priority",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "mapd_available",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
"label": "Car State Priority"
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"label": "Map Data Priority",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "mapd_available",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
"label": "Map Data Priority"
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"label": "Combined",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "mapd_available",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
"label": "Combined"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1029,11 +1001,6 @@
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "mapd_available",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "any",
|
||||
"conditions": [
|
||||
@@ -2054,8 +2021,14 @@
|
||||
{
|
||||
"id": "model_behavior",
|
||||
"title": "Model Behavior",
|
||||
"description": "Lane desire and lead-vehicle awareness tuning",
|
||||
"description": "Lane desire, Blendful lane centering, and lead-vehicle awareness tuning",
|
||||
"items": [
|
||||
{
|
||||
"key": "Blendful",
|
||||
"widget": "toggle",
|
||||
"title": "Blendful Mode",
|
||||
"description": "Nudges the car toward the lane center when the lane lines are clear. Drives laneless when they are not."
|
||||
},
|
||||
{
|
||||
"key": "LaneTurnDesire",
|
||||
"widget": "toggle",
|
||||
|
||||
@@ -167,28 +167,12 @@ sections:
|
||||
label: Car State Only
|
||||
- value: 1
|
||||
label: Map Data Only
|
||||
enablement:
|
||||
- type: capability
|
||||
field: mapd_available
|
||||
equals: true
|
||||
- value: 2
|
||||
label: Car State Priority
|
||||
enablement:
|
||||
- type: capability
|
||||
field: mapd_available
|
||||
equals: true
|
||||
- value: 3
|
||||
label: Map Data Priority
|
||||
enablement:
|
||||
- type: capability
|
||||
field: mapd_available
|
||||
equals: true
|
||||
- value: 4
|
||||
label: Combined
|
||||
enablement:
|
||||
- type: capability
|
||||
field: mapd_available
|
||||
equals: true
|
||||
- key: SpeedLimitOffsetType
|
||||
widget: multiple_button
|
||||
title: Speed Limit Offset Type
|
||||
@@ -262,9 +246,6 @@ sections:
|
||||
field: has_icbm
|
||||
equals: true
|
||||
enablement:
|
||||
- type: capability
|
||||
field: mapd_available
|
||||
equals: true
|
||||
- type: any
|
||||
conditions:
|
||||
- type: capability
|
||||
|
||||
@@ -9,8 +9,12 @@ description: Driving model behavior and camera calibration
|
||||
sections:
|
||||
- id: model_behavior
|
||||
title: Model Behavior
|
||||
description: Lane desire and lead-vehicle awareness tuning
|
||||
description: Lane desire, Blendful lane centering, and lead-vehicle awareness tuning
|
||||
items:
|
||||
- key: Blendful
|
||||
widget: toggle
|
||||
title: Blendful Mode
|
||||
description: Nudges the car toward the lane center when the lane lines are clear. Drives laneless when they are not.
|
||||
- key: LaneTurnDesire
|
||||
widget: toggle
|
||||
title: Use Lane Turn Desires
|
||||
|
||||
@@ -57,7 +57,7 @@ class DownloadStatusAction(ItemAction):
|
||||
self.icon: str | None = None
|
||||
self.icon_color: rl.Color | None = None
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
# raw progress arrives in steps, one per 128KB chunk the manager publishes
|
||||
# raw progress arrives in steps, one per progress report from the manager
|
||||
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
||||
# integrated per frame; (t * speed) % span jumps whenever the fill width changes
|
||||
self._sweep = 0.0
|
||||
|
||||
+1
-1
Submodule panda updated: 67788c3db2...8a16e089b8
+1
-1
Submodule tinygrad_repo updated: 69fd4d34d4...92b742f711
Reference in New Issue
Block a user