mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-10 13:34:21 +08:00
Compare commits
2 Commits
tune
...
sunnymodel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88dc1ac3bc | ||
|
|
d7e5f3cf43 |
@@ -140,7 +140,7 @@ jobs:
|
||||
run: |
|
||||
echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json
|
||||
built=(); while IFS= read -r line; do built+=("$line"); done < <(
|
||||
find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
|
||||
ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
|
||||
)
|
||||
jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \
|
||||
'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json
|
||||
@@ -168,7 +168,6 @@ jobs:
|
||||
if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
model: ${{ fromJson(needs.setup.outputs.model_matrix) }}
|
||||
|
||||
39
.github/workflows/jenkins-pr-trigger.yaml
vendored
39
.github/workflows/jenkins-pr-trigger.yaml
vendored
@@ -5,44 +5,7 @@ on:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
cleanup-branches:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Delete stale Jenkins branches
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
||||
const prefixes = ['tmp-jenkins', '__jenkins'];
|
||||
|
||||
for await (const response of github.paginate.iterator(github.rest.repos.listBranches, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100,
|
||||
})) {
|
||||
for (const branch of response.data) {
|
||||
if (!prefixes.some(p => branch.name.startsWith(p))) continue;
|
||||
|
||||
const { data: commit } = await github.rest.repos.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: branch.commit.sha,
|
||||
});
|
||||
|
||||
const commitDate = new Date(commit.commit.committer.date).getTime();
|
||||
if (commitDate < cutoff) {
|
||||
console.log(`Deleting branch: ${branch.name} (last commit: ${commit.commit.committer.date})`);
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${branch.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# TODO: gc old branches in a separate job in this workflow
|
||||
scan-comments:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request }}
|
||||
|
||||
21
.github/workflows/repo-maintenance.yaml
vendored
21
.github/workflows/repo-maintenance.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
run: |
|
||||
${{ env.RUN }} "python3 selfdrive/ui/update_translations.py --vanish"
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
with:
|
||||
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
commit-message: "Update translations"
|
||||
@@ -43,22 +43,14 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
- name: uv lock
|
||||
if: github.repository == 'commaai/openpilot'
|
||||
run: |
|
||||
python3 -m ensurepip --upgrade
|
||||
pip3 install uv
|
||||
uv lock --upgrade
|
||||
- name: uv pip tree
|
||||
id: pip_tree
|
||||
run: |
|
||||
echo 'PIP_TREE<<EOF' >> $GITHUB_OUTPUT
|
||||
uv pip tree >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
- name: bump submodules
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
git config submodule.msgq.update none
|
||||
git config submodule.rednose_repo.update none
|
||||
git config submodule.teleoprtc_repo.update none
|
||||
git config submodule.tinygrad.update none
|
||||
git submodule update --remote
|
||||
git add .
|
||||
@@ -69,7 +61,7 @@ jobs:
|
||||
python selfdrive/car/docs.py
|
||||
git add docs/CARS.md
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
with:
|
||||
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }}
|
||||
@@ -78,10 +70,5 @@ jobs:
|
||||
branch: auto-package-updates
|
||||
base: master
|
||||
delete-branch: true
|
||||
body: |
|
||||
Automatic PR from repo-maintenance -> package_updates
|
||||
|
||||
```
|
||||
${{ steps.pip_tree.outputs.PIP_TREE }}
|
||||
```
|
||||
body: 'Automatic PR from repo-maintenance -> package_updates'
|
||||
labels: bot
|
||||
|
||||
28
.github/workflows/tests.yaml
vendored
28
.github/workflows/tests.yaml
vendored
@@ -20,6 +20,8 @@ concurrency:
|
||||
env:
|
||||
PYTHONWARNINGS: error
|
||||
BASE_IMAGE: sunnypilot-base
|
||||
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
|
||||
|
||||
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD: release/ci/docker_build_sp.sh base
|
||||
|
||||
@@ -105,7 +107,6 @@ jobs:
|
||||
|
||||
build_mac:
|
||||
name: build macOS
|
||||
if: false # tmp disable due to brew install not working
|
||||
runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -215,13 +216,12 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .ci_cache/comma_download_cache
|
||||
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/test_processes.py') }}
|
||||
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }}
|
||||
- name: Build openpilot
|
||||
run: |
|
||||
${{ env.RUN }} "scons -j$(nproc)"
|
||||
- name: Run replay
|
||||
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
|
||||
continue-on-error: ${{ github.ref == 'refs/heads/master' }}
|
||||
run: |
|
||||
${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
|
||||
chmod -R 777 /tmp/comma_download_cache"
|
||||
@@ -235,26 +235,10 @@ jobs:
|
||||
with:
|
||||
name: process_replay_diff.txt
|
||||
path: selfdrive/test/process_replay/diff.txt
|
||||
- name: Checkout ci-artifacts
|
||||
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: commaai/ci-artifacts
|
||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
|
||||
path: ${{ github.workspace }}/ci-artifacts
|
||||
- name: Push refs
|
||||
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
|
||||
working-directory: ${{ github.workspace }}/ci-artifacts
|
||||
- name: Upload reference logs
|
||||
if: false # TODO: move this to github instead of azure
|
||||
run: |
|
||||
git checkout --orphan process-replay
|
||||
git rm -rf .
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
cp ${{ github.workspace }}/selfdrive/test/process_replay/fakedata/*.zst .
|
||||
echo "${{ github.sha }}" > ref_commit
|
||||
git add .
|
||||
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}"
|
||||
git push origin process-replay --force
|
||||
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
|
||||
- name: Run regen
|
||||
if: false
|
||||
timeout-minutes: 4
|
||||
|
||||
51
.github/workflows/vendor_third_party.yaml
vendored
51
.github/workflows/vendor_third_party.yaml
vendored
@@ -1,51 +0,0 @@
|
||||
name: vendor third_party
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref != 'refs/heads/master'
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-24.04, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- name: Build
|
||||
run: third_party/build.sh
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
git add -A third_party/
|
||||
git diff --cached --name-only -- third_party/ | tar -cf /tmp/third_party_build.tar -T -
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: third-party-${{ runner.os }}
|
||||
path: /tmp/third_party_build.tar
|
||||
|
||||
commit:
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/artifacts
|
||||
- name: Commit vendored libraries
|
||||
run: |
|
||||
for f in /tmp/artifacts/*/third_party_build.tar; do
|
||||
tar xf "$f"
|
||||
done
|
||||
git add third_party/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "third_party: rebuild vendor libraries"
|
||||
git push
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -100,7 +100,6 @@ Pipfile
|
||||
.ionide
|
||||
|
||||
.claude/
|
||||
.context/
|
||||
PLAN.md
|
||||
TASK.md
|
||||
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -15,7 +15,7 @@
|
||||
url = https://github.com/commaai/teleoprtc
|
||||
[submodule "tinygrad"]
|
||||
path = tinygrad_repo
|
||||
url = https://github.com/sunnypilot/tinygrad.git
|
||||
url = https://github.com/commaai/tinygrad.git
|
||||
[submodule "sunnypilot/neural_network_data"]
|
||||
path = sunnypilot/neural_network_data
|
||||
url = https://github.com/sunnypilot/neural-network-data.git
|
||||
|
||||
@@ -9,6 +9,4 @@ WORKDIR ${OPENPILOT_PATH}
|
||||
|
||||
COPY . ${OPENPILOT_PATH}/
|
||||
|
||||
ENV UV_BIN="/home/batman/.local/bin/"
|
||||
ENV PATH="$UV_BIN:$PATH"
|
||||
RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc)
|
||||
RUN scons --cache-readonly -j$(nproc)
|
||||
|
||||
@@ -13,7 +13,7 @@ cereal = env.Library('cereal', [f'gen/cpp/{s}.c++' for s in schema_files])
|
||||
|
||||
# Build messaging
|
||||
services_h = env.Command(['services.h'], ['services.py'], 'python3 ' + cereal_dir.path + '/services.py > $TARGET')
|
||||
env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc', 'messaging/bridge_zmq.cc'], LIBS=[msgq, common, 'pthread'])
|
||||
env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc'], LIBS=[msgq, common, 'pthread'])
|
||||
|
||||
socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc'])
|
||||
|
||||
|
||||
@@ -1478,11 +1478,6 @@ struct ProcLog {
|
||||
|
||||
cmdline @15 :List(Text);
|
||||
exe @16 :Text;
|
||||
|
||||
# from /proc/<pid>/smaps_rollup (proportional/private memory)
|
||||
memPss @17 :UInt64; # Pss — shared pages split by mapper count
|
||||
memPssAnon @18 :UInt64; # Pss_Anon — private anonymous (heap, stack)
|
||||
memPssShmem @19 :UInt64; # Pss_Shmem — proportional MSGQ/tmpfs share
|
||||
}
|
||||
|
||||
struct CPUTimes {
|
||||
@@ -2232,9 +2227,9 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
|
||||
isActiveMode @16 :Bool;
|
||||
isRHD @4 :Bool;
|
||||
uncertainCount @19 :UInt32;
|
||||
phoneProbOffset @20 :Float32;
|
||||
phoneProbValidCount @21 :UInt32;
|
||||
|
||||
phoneProbOffsetDEPRECATED @20 :Float32;
|
||||
phoneProbValidCountDEPRECATED @21 :UInt32;
|
||||
isPreviewDEPRECATED @15 :Bool;
|
||||
rhdCheckedDEPRECATED @5 :Bool;
|
||||
eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED);
|
||||
|
||||
@@ -25,14 +25,14 @@ void msgq_to_zmq(const std::vector<std::string> &endpoints, const std::string &i
|
||||
}
|
||||
|
||||
void zmq_to_msgq(const std::vector<std::string> &endpoints, const std::string &ip) {
|
||||
auto poller = std::make_unique<BridgeZmqPoller>();
|
||||
auto pub_context = std::make_unique<Context>();
|
||||
auto sub_context = std::make_unique<BridgeZmqContext>();
|
||||
std::map<BridgeZmqSubSocket *, PubSocket *> sub2pub;
|
||||
auto poller = std::make_unique<ZMQPoller>();
|
||||
auto pub_context = std::make_unique<MSGQContext>();
|
||||
auto sub_context = std::make_unique<ZMQContext>();
|
||||
std::map<SubSocket *, PubSocket *> sub2pub;
|
||||
|
||||
for (auto endpoint : endpoints) {
|
||||
auto pub_sock = new PubSocket();
|
||||
auto sub_sock = new BridgeZmqSubSocket();
|
||||
auto pub_sock = new MSGQPubSocket();
|
||||
auto sub_sock = new ZMQSubSocket();
|
||||
size_t queue_size = services.at(endpoint).queue_size;
|
||||
pub_sock->connect(pub_context.get(), endpoint, true, queue_size);
|
||||
sub_sock->connect(sub_context.get(), endpoint, ip, false);
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
#include "cereal/messaging/bridge_zmq.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
|
||||
static size_t fnv1a_hash(const std::string &str) {
|
||||
const size_t fnv_prime = 0x100000001b3;
|
||||
size_t hash_value = 0xcbf29ce484222325;
|
||||
for (char c : str) {
|
||||
hash_value ^= (unsigned char)c;
|
||||
hash_value *= fnv_prime;
|
||||
}
|
||||
return hash_value;
|
||||
}
|
||||
|
||||
// FIXME: This is a hack to get the port number from the socket name, might have collisions.
|
||||
static int get_port(std::string endpoint) {
|
||||
size_t hash_value = fnv1a_hash(endpoint);
|
||||
int start_port = 8023;
|
||||
int max_port = 65535;
|
||||
return start_port + (hash_value % (max_port - start_port));
|
||||
}
|
||||
|
||||
BridgeZmqContext::BridgeZmqContext() {
|
||||
context = zmq_ctx_new();
|
||||
}
|
||||
|
||||
BridgeZmqContext::~BridgeZmqContext() {
|
||||
if (context != nullptr) {
|
||||
zmq_ctx_term(context);
|
||||
}
|
||||
}
|
||||
|
||||
void BridgeZmqMessage::init(size_t sz) {
|
||||
size = sz;
|
||||
data = new char[size];
|
||||
}
|
||||
|
||||
void BridgeZmqMessage::init(char *d, size_t sz) {
|
||||
size = sz;
|
||||
data = new char[size];
|
||||
memcpy(data, d, size);
|
||||
}
|
||||
|
||||
void BridgeZmqMessage::close() {
|
||||
if (size > 0) {
|
||||
delete[] data;
|
||||
}
|
||||
data = nullptr;
|
||||
size = 0;
|
||||
}
|
||||
|
||||
BridgeZmqMessage::~BridgeZmqMessage() {
|
||||
close();
|
||||
}
|
||||
|
||||
int BridgeZmqSubSocket::connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate, bool check_endpoint) {
|
||||
sock = zmq_socket(context->getRawContext(), ZMQ_SUB);
|
||||
if (sock == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
zmq_setsockopt(sock, ZMQ_SUBSCRIBE, "", 0);
|
||||
|
||||
if (conflate) {
|
||||
int arg = 1;
|
||||
zmq_setsockopt(sock, ZMQ_CONFLATE, &arg, sizeof(int));
|
||||
}
|
||||
|
||||
int reconnect_ivl = 500;
|
||||
zmq_setsockopt(sock, ZMQ_RECONNECT_IVL_MAX, &reconnect_ivl, sizeof(reconnect_ivl));
|
||||
|
||||
full_endpoint = "tcp://" + address + ":";
|
||||
if (check_endpoint) {
|
||||
full_endpoint += std::to_string(get_port(endpoint));
|
||||
} else {
|
||||
full_endpoint += endpoint;
|
||||
}
|
||||
|
||||
return zmq_connect(sock, full_endpoint.c_str());
|
||||
}
|
||||
|
||||
void BridgeZmqSubSocket::setTimeout(int timeout) {
|
||||
zmq_setsockopt(sock, ZMQ_RCVTIMEO, &timeout, sizeof(int));
|
||||
}
|
||||
|
||||
Message *BridgeZmqSubSocket::receive(bool non_blocking) {
|
||||
zmq_msg_t msg;
|
||||
assert(zmq_msg_init(&msg) == 0);
|
||||
|
||||
int flags = non_blocking ? ZMQ_DONTWAIT : 0;
|
||||
int rc = zmq_msg_recv(&msg, sock, flags);
|
||||
|
||||
Message *ret = nullptr;
|
||||
if (rc >= 0) {
|
||||
ret = new BridgeZmqMessage;
|
||||
ret->init((char *)zmq_msg_data(&msg), zmq_msg_size(&msg));
|
||||
}
|
||||
|
||||
zmq_msg_close(&msg);
|
||||
return ret;
|
||||
}
|
||||
|
||||
BridgeZmqSubSocket::~BridgeZmqSubSocket() {
|
||||
if (sock != nullptr) {
|
||||
zmq_close(sock);
|
||||
}
|
||||
}
|
||||
|
||||
int BridgeZmqPubSocket::connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint) {
|
||||
sock = zmq_socket(context->getRawContext(), ZMQ_PUB);
|
||||
if (sock == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
full_endpoint = "tcp://*:";
|
||||
if (check_endpoint) {
|
||||
full_endpoint += std::to_string(get_port(endpoint));
|
||||
} else {
|
||||
full_endpoint += endpoint;
|
||||
}
|
||||
|
||||
// ZMQ pub sockets cannot be shared between processes, so we need to ensure pid stays the same.
|
||||
pid = getpid();
|
||||
|
||||
return zmq_bind(sock, full_endpoint.c_str());
|
||||
}
|
||||
|
||||
int BridgeZmqPubSocket::sendMessage(Message *message) {
|
||||
assert(pid == getpid());
|
||||
return zmq_send(sock, message->getData(), message->getSize(), ZMQ_DONTWAIT);
|
||||
}
|
||||
|
||||
int BridgeZmqPubSocket::send(char *data, size_t size) {
|
||||
assert(pid == getpid());
|
||||
return zmq_send(sock, data, size, ZMQ_DONTWAIT);
|
||||
}
|
||||
|
||||
BridgeZmqPubSocket::~BridgeZmqPubSocket() {
|
||||
if (sock != nullptr) {
|
||||
zmq_close(sock);
|
||||
}
|
||||
}
|
||||
|
||||
void BridgeZmqPoller::registerSocket(BridgeZmqSubSocket *socket) {
|
||||
assert(num_polls + 1 < (sizeof(polls) / sizeof(polls[0])));
|
||||
polls[num_polls].socket = socket->getRawSocket();
|
||||
polls[num_polls].events = ZMQ_POLLIN;
|
||||
|
||||
sockets.push_back(socket);
|
||||
num_polls++;
|
||||
}
|
||||
|
||||
std::vector<BridgeZmqSubSocket *> BridgeZmqPoller::poll(int timeout) {
|
||||
std::vector<BridgeZmqSubSocket *> ret;
|
||||
|
||||
int rc = zmq_poll(polls, num_polls, timeout);
|
||||
if (rc < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_polls; i++) {
|
||||
if (polls[i].revents) {
|
||||
ret.push_back(sockets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <zmq.h>
|
||||
|
||||
#include "msgq/ipc.h"
|
||||
|
||||
class BridgeZmqContext {
|
||||
public:
|
||||
BridgeZmqContext();
|
||||
void *getRawContext() { return context; }
|
||||
~BridgeZmqContext();
|
||||
|
||||
private:
|
||||
void *context = nullptr;
|
||||
};
|
||||
|
||||
class BridgeZmqMessage : public Message {
|
||||
public:
|
||||
void init(size_t size);
|
||||
void init(char *data, size_t size);
|
||||
void close();
|
||||
size_t getSize() { return size; }
|
||||
char *getData() { return data; }
|
||||
~BridgeZmqMessage();
|
||||
|
||||
private:
|
||||
char *data = nullptr;
|
||||
size_t size = 0;
|
||||
};
|
||||
|
||||
class BridgeZmqSubSocket {
|
||||
public:
|
||||
int connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate = false, bool check_endpoint = true);
|
||||
void setTimeout(int timeout);
|
||||
Message *receive(bool non_blocking = false);
|
||||
void *getRawSocket() { return sock; }
|
||||
~BridgeZmqSubSocket();
|
||||
|
||||
private:
|
||||
void *sock = nullptr;
|
||||
std::string full_endpoint;
|
||||
};
|
||||
|
||||
class BridgeZmqPubSocket {
|
||||
public:
|
||||
int connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint = true);
|
||||
int sendMessage(Message *message);
|
||||
int send(char *data, size_t size);
|
||||
void *getRawSocket() { return sock; }
|
||||
~BridgeZmqPubSocket();
|
||||
|
||||
private:
|
||||
void *sock = nullptr;
|
||||
std::string full_endpoint;
|
||||
int pid = -1;
|
||||
};
|
||||
|
||||
class BridgeZmqPoller {
|
||||
public:
|
||||
void registerSocket(BridgeZmqSubSocket *socket);
|
||||
std::vector<BridgeZmqSubSocket *> poll(int timeout);
|
||||
|
||||
private:
|
||||
static constexpr size_t MAX_BRIDGE_ZMQ_POLLERS = 128;
|
||||
std::vector<BridgeZmqSubSocket *> sockets;
|
||||
zmq_pollitem_t polls[MAX_BRIDGE_ZMQ_POLLERS] = {};
|
||||
size_t num_polls = 0;
|
||||
};
|
||||
@@ -22,14 +22,14 @@ static std::string recv_zmq_msg(void *sock) {
|
||||
}
|
||||
|
||||
void MsgqToZmq::run(const std::vector<std::string> &endpoints, const std::string &ip) {
|
||||
zmq_context = std::make_unique<BridgeZmqContext>();
|
||||
msgq_context = std::make_unique<Context>();
|
||||
zmq_context = std::make_unique<ZMQContext>();
|
||||
msgq_context = std::make_unique<MSGQContext>();
|
||||
|
||||
// Create ZMQPubSockets for each endpoint
|
||||
for (const auto &endpoint : endpoints) {
|
||||
auto &socket_pair = socket_pairs.emplace_back();
|
||||
socket_pair.endpoint = endpoint;
|
||||
socket_pair.pub_sock = std::make_unique<BridgeZmqPubSocket>();
|
||||
socket_pair.pub_sock = std::make_unique<ZMQPubSocket>();
|
||||
int ret = socket_pair.pub_sock->connect(zmq_context.get(), endpoint);
|
||||
if (ret != 0) {
|
||||
printf("Failed to create ZMQ publisher for [%s]: %s\n", endpoint.c_str(), zmq_strerror(zmq_errno()));
|
||||
@@ -49,7 +49,7 @@ void MsgqToZmq::run(const std::vector<std::string> &endpoints, const std::string
|
||||
|
||||
for (auto sub_sock : msgq_poller->poll(100)) {
|
||||
// Process messages for each socket
|
||||
BridgeZmqPubSocket *pub_sock = sub2pub.at(sub_sock);
|
||||
ZMQPubSocket *pub_sock = sub2pub.at(sub_sock);
|
||||
for (int i = 0; i < MAX_MESSAGES_PER_SOCKET; ++i) {
|
||||
auto msg = std::unique_ptr<Message>(sub_sock->receive(true));
|
||||
if (!msg) break;
|
||||
@@ -72,7 +72,7 @@ void MsgqToZmq::zmqMonitorThread() {
|
||||
// Set up ZMQ monitor for each pub socket
|
||||
for (int i = 0; i < socket_pairs.size(); ++i) {
|
||||
std::string addr = "inproc://op-bridge-monitor-" + std::to_string(i);
|
||||
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED);
|
||||
zmq_socket_monitor(socket_pairs[i].pub_sock->sock, addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED);
|
||||
|
||||
void *monitor_socket = zmq_socket(zmq_context->getRawContext(), ZMQ_PAIR);
|
||||
zmq_connect(monitor_socket, addr.c_str());
|
||||
@@ -130,7 +130,7 @@ void MsgqToZmq::zmqMonitorThread() {
|
||||
|
||||
// Clean up monitor sockets
|
||||
for (int i = 0; i < pollitems.size(); ++i) {
|
||||
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), nullptr, 0);
|
||||
zmq_socket_monitor(socket_pairs[i].pub_sock->sock, nullptr, 0);
|
||||
zmq_close(pollitems[i].socket);
|
||||
}
|
||||
cv.notify_one();
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define private public
|
||||
#include "msgq/impl_msgq.h"
|
||||
#include "cereal/messaging/bridge_zmq.h"
|
||||
#include "msgq/impl_zmq.h"
|
||||
|
||||
class MsgqToZmq {
|
||||
public:
|
||||
@@ -21,16 +22,16 @@ protected:
|
||||
|
||||
struct SocketPair {
|
||||
std::string endpoint;
|
||||
std::unique_ptr<BridgeZmqPubSocket> pub_sock;
|
||||
std::unique_ptr<ZMQPubSocket> pub_sock;
|
||||
std::unique_ptr<MSGQSubSocket> sub_sock;
|
||||
int connected_clients = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<Context> msgq_context;
|
||||
std::unique_ptr<BridgeZmqContext> zmq_context;
|
||||
std::unique_ptr<MSGQContext> msgq_context;
|
||||
std::unique_ptr<ZMQContext> zmq_context;
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::unique_ptr<MSGQPoller> msgq_poller;
|
||||
std::map<SubSocket *, BridgeZmqPubSocket *> sub2pub;
|
||||
std::map<SubSocket *, ZMQPubSocket *> sub2pub;
|
||||
std::vector<SocketPair> socket_pairs;
|
||||
};
|
||||
|
||||
@@ -137,7 +137,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
|
||||
{"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h
|
||||
{"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"Brightness", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
@@ -230,6 +229,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
|
||||
{"ModeldSunny", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AlpamayoDriveFast", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// mapd
|
||||
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
|
||||
|
||||
@@ -167,92 +167,6 @@ def managed_proc(cmd: list[str], env: dict[str, str]):
|
||||
proc.kill()
|
||||
|
||||
|
||||
def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None):
|
||||
rows = [list(row) for row in tabular_data]
|
||||
|
||||
def fmt(val):
|
||||
if isinstance(val, str):
|
||||
return val
|
||||
if isinstance(val, (bool, int)):
|
||||
return str(val)
|
||||
try:
|
||||
return format(val, floatfmt)
|
||||
except (TypeError, ValueError):
|
||||
return str(val)
|
||||
|
||||
formatted = [[fmt(c) for c in row] for row in rows]
|
||||
hdrs = [str(h) for h in headers] if headers else None
|
||||
|
||||
ncols = max((len(r) for r in formatted), default=0)
|
||||
if hdrs:
|
||||
ncols = max(ncols, len(hdrs))
|
||||
if ncols == 0:
|
||||
return ""
|
||||
|
||||
for r in formatted:
|
||||
r.extend([""] * (ncols - len(r)))
|
||||
if hdrs:
|
||||
hdrs.extend([""] * (ncols - len(hdrs)))
|
||||
|
||||
widths = [0] * ncols
|
||||
if hdrs:
|
||||
for i in range(ncols):
|
||||
widths[i] = len(hdrs[i])
|
||||
for row in formatted:
|
||||
for i in range(ncols):
|
||||
widths[i] = max(widths[i], max(len(ln) for ln in row[i].split('\n')))
|
||||
|
||||
def _align(s, w):
|
||||
if stralign == "center":
|
||||
return s.center(w)
|
||||
return s.ljust(w)
|
||||
|
||||
if tablefmt == "html":
|
||||
parts = ["<table>"]
|
||||
if hdrs:
|
||||
parts.append("<thead>")
|
||||
parts.append("<tr>" + "".join(f"<th>{h}</th>" for h in hdrs) + "</tr>")
|
||||
parts.append("</thead>")
|
||||
parts.append("<tbody>")
|
||||
for row in formatted:
|
||||
parts.append("<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>")
|
||||
parts.append("</tbody>")
|
||||
parts.append("</table>")
|
||||
return "\n".join(parts)
|
||||
|
||||
if tablefmt == "simple_grid":
|
||||
def _sep(left, mid, right):
|
||||
return left + mid.join("\u2500" * (w + 2) for w in widths) + right
|
||||
|
||||
top, mid_sep, bot = _sep("\u250c", "\u252c", "\u2510"), _sep("\u251c", "\u253c", "\u2524"), _sep("\u2514", "\u2534", "\u2518")
|
||||
|
||||
def _fmt_row(cells):
|
||||
split = [c.split('\n') for c in cells]
|
||||
nlines = max(len(s) for s in split)
|
||||
for s in split:
|
||||
s.extend([""] * (nlines - len(s)))
|
||||
return ["\u2502" + "\u2502".join(f" {_align(split[i][li], widths[i])} " for i in range(ncols)) + "\u2502" for li in range(nlines)]
|
||||
|
||||
lines = [top]
|
||||
if hdrs:
|
||||
lines.extend(_fmt_row(hdrs))
|
||||
lines.append(mid_sep)
|
||||
for ri, row in enumerate(formatted):
|
||||
lines.extend(_fmt_row(row))
|
||||
lines.append(mid_sep if ri < len(formatted) - 1 else bot)
|
||||
return "\n".join(lines)
|
||||
|
||||
# simple
|
||||
gap = " "
|
||||
lines = []
|
||||
if hdrs:
|
||||
lines.append(gap.join(h.ljust(w) for h, w in zip(hdrs, widths, strict=True)))
|
||||
lines.append(gap.join("-" * w for w in widths))
|
||||
for row in formatted:
|
||||
lines.append(gap.join(_align(row[i], widths[i]) for i in range(ncols)))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def retry(attempts=3, delay=1.0, ignore_failure=False):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
|
||||
Submodule msgq_repo updated: 4c4e814ed5...20f2493855
Submodule opendbc_repo updated: 383a720260...ff2f9686c2
2
panda
2
panda
Submodule panda updated: a95e060e85...ed8a6f9ec2
@@ -89,6 +89,7 @@ testing = [
|
||||
"pytest-timeout",
|
||||
"pytest-asyncio",
|
||||
"pytest-mock",
|
||||
"pytest-repeat",
|
||||
"ruff",
|
||||
"codespell",
|
||||
"pre-commit-hooks",
|
||||
@@ -96,12 +97,15 @@ testing = [
|
||||
|
||||
dev = [
|
||||
"av",
|
||||
"azure-identity",
|
||||
"azure-storage-blob",
|
||||
"dictdiffer",
|
||||
"matplotlib",
|
||||
"opencv-python-headless",
|
||||
"parameterized >=0.8, <0.9",
|
||||
"pyautogui",
|
||||
"pywinctl",
|
||||
"tabulate",
|
||||
]
|
||||
|
||||
tools = [
|
||||
|
||||
@@ -11,7 +11,7 @@ LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
|
||||
|
||||
GLYPH_PADDING = 6
|
||||
EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥"
|
||||
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
||||
UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
||||
|
||||
|
||||
def _languages():
|
||||
|
||||
BIN
selfdrive/assets/icons_mici/buttons/button_side_back.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/buttons/button_side_back.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/buttons/button_side_back_pressed.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/buttons/button_side_back_pressed.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/buttons/button_side_check.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/buttons/button_side_check.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/buttons/button_side_check_pressed.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/buttons/button_side_check_pressed.png
LFS
Normal file
Binary file not shown.
Binary file not shown.
BIN
selfdrive/assets/icons_mici/settings/keyboard/confirm.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/settings/keyboard/confirm.png
LFS
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
selfdrive/assets/icons_mici/settings/vertical_scroll_indicator.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/settings/vertical_scroll_indicator.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/tethering_short.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/tethering_short.png
LFS
Normal file
Binary file not shown.
@@ -22,8 +22,7 @@ LONG_MPC_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
EXPORT_DIR = os.path.join(LONG_MPC_DIR, "c_generated_code")
|
||||
JSON_FILE = os.path.join(LONG_MPC_DIR, "acados_ocp_long.json")
|
||||
|
||||
LongitudinalPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
|
||||
MPC_SOURCES = (LongitudinalPlanSource.lead0, LongitudinalPlanSource.lead1, LongitudinalPlanSource.cruise)
|
||||
SOURCES = ['lead0', 'lead1', 'cruise', 'e2e']
|
||||
|
||||
X_DIM = 3
|
||||
U_DIM = 1
|
||||
@@ -108,10 +107,10 @@ def gen_long_model():
|
||||
a_min = SX.sym('a_min')
|
||||
a_max = SX.sym('a_max')
|
||||
x_obstacle = SX.sym('x_obstacle')
|
||||
a_prev = SX.sym('a_prev')
|
||||
prev_a = SX.sym('prev_a')
|
||||
lead_t_follow = SX.sym('lead_t_follow')
|
||||
lead_danger_factor = SX.sym('lead_danger_factor')
|
||||
model.p = vertcat(a_min, a_max, x_obstacle, a_prev, lead_t_follow, lead_danger_factor)
|
||||
model.p = vertcat(a_min, a_max, x_obstacle, prev_a, lead_t_follow, lead_danger_factor)
|
||||
|
||||
# dynamics model
|
||||
f_expl = vertcat(v_ego, a_ego, j_ego)
|
||||
@@ -143,7 +142,7 @@ def gen_long_ocp():
|
||||
|
||||
a_min, a_max = ocp.model.p[0], ocp.model.p[1]
|
||||
x_obstacle = ocp.model.p[2]
|
||||
a_prev = ocp.model.p[3]
|
||||
prev_a = ocp.model.p[3]
|
||||
lead_t_follow = ocp.model.p[4]
|
||||
lead_danger_factor = ocp.model.p[5]
|
||||
|
||||
@@ -160,7 +159,7 @@ def gen_long_ocp():
|
||||
x_ego,
|
||||
v_ego,
|
||||
a_ego,
|
||||
a_ego - a_prev,
|
||||
a_ego - prev_a,
|
||||
j_ego]
|
||||
ocp.model.cost_y_expr = vertcat(*costs)
|
||||
ocp.model.cost_y_expr_e = vertcat(*costs[:-1])
|
||||
@@ -218,7 +217,7 @@ class LongitudinalMpc:
|
||||
self.dt = dt
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = LongitudinalPlanSource.cruise
|
||||
self.source = SOURCES[2]
|
||||
|
||||
def reset(self):
|
||||
self.solver.reset()
|
||||
@@ -228,7 +227,7 @@ class LongitudinalMpc:
|
||||
self.v_solution = np.zeros(N+1)
|
||||
self.a_solution = np.zeros(N+1)
|
||||
self.j_solution = np.zeros(N)
|
||||
self.a_prev = np.array(self.a_solution)
|
||||
self.prev_a = np.array(self.a_solution)
|
||||
self.yref = np.zeros((N+1, COST_DIM))
|
||||
|
||||
for i in range(N):
|
||||
@@ -336,7 +335,7 @@ class LongitudinalMpc:
|
||||
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
|
||||
|
||||
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
|
||||
self.source = MPC_SOURCES[np.argmin(x_obstacles[0])]
|
||||
self.source = SOURCES[np.argmin(x_obstacles[0])]
|
||||
|
||||
self.yref[:,:] = 0.0
|
||||
for i in range(N):
|
||||
@@ -346,7 +345,7 @@ class LongitudinalMpc:
|
||||
self.params[:,0] = ACCEL_MIN
|
||||
self.params[:,1] = ACCEL_MAX
|
||||
self.params[:,2] = np.min(x_obstacles, axis=1)
|
||||
self.params[:,3] = np.copy(self.a_prev)
|
||||
self.params[:,3] = np.copy(self.prev_a)
|
||||
self.params[:,4] = t_follow
|
||||
self.params[:,5] = LEAD_DANGER_FACTOR
|
||||
|
||||
@@ -378,7 +377,7 @@ class LongitudinalMpc:
|
||||
self.a_solution = self.x_sol[:,2]
|
||||
self.j_solution = self.u_sol[:,0]
|
||||
|
||||
self.a_prev = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
|
||||
self.prev_a = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
|
||||
|
||||
t = time.monotonic()
|
||||
if self.solution_status != 0:
|
||||
|
||||
@@ -9,7 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
|
||||
@@ -164,7 +164,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
output_a_target = min(output_a_target_e2e, output_a_target_mpc)
|
||||
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
||||
if output_a_target < output_a_target_mpc:
|
||||
self.mpc.source = LongitudinalPlanSource.e2e
|
||||
self.mpc.source = SOURCES[3]
|
||||
else:
|
||||
output_a_target = output_a_target_mpc
|
||||
self.output_should_stop = output_should_stop_mpc
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
from tabulate import tabulate
|
||||
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
|
||||
DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19"
|
||||
MB = 1024 * 1024
|
||||
TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center")
|
||||
|
||||
|
||||
def _get_procs():
|
||||
from openpilot.selfdrive.test.test_onroad import PROCS
|
||||
return PROCS
|
||||
|
||||
|
||||
def is_openpilot_proc(name):
|
||||
if any(p in name for p in _get_procs()):
|
||||
return True
|
||||
# catch openpilot processes not in PROCS (athenad, manager, etc.)
|
||||
return 'openpilot' in name or name.startswith(('selfdrive.', 'system.'))
|
||||
|
||||
|
||||
def get_proc_name(proc):
|
||||
if len(proc.cmdline) > 0:
|
||||
return list(proc.cmdline)[0]
|
||||
return proc.name
|
||||
|
||||
|
||||
def pct(val_mb, total_mb):
|
||||
return val_mb / total_mb * 100 if total_mb else 0
|
||||
|
||||
|
||||
def has_pss(proc_logs):
|
||||
"""Check if logs contain PSS data (new field, not in old logs)."""
|
||||
try:
|
||||
for proc in proc_logs[-1].procLog.procs:
|
||||
if proc.memPss > 0:
|
||||
return True
|
||||
except AttributeError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def print_summary(proc_logs, device_states):
|
||||
mem = proc_logs[-1].procLog.mem
|
||||
total = mem.total / MB
|
||||
used = (mem.total - mem.available) / MB
|
||||
cached = mem.cached / MB
|
||||
shared = mem.shared / MB
|
||||
buffers = mem.buffers / MB
|
||||
|
||||
lines = [
|
||||
f" Total: {total:.0f} MB",
|
||||
f" Used (total-avail): {used:.0f} MB ({pct(used, total):.0f}%)",
|
||||
f" Cached: {cached:.0f} MB ({pct(cached, total):.0f}%) Buffers: {buffers:.0f} MB ({pct(buffers, total):.0f}%)",
|
||||
f" Shared/MSGQ: {shared:.0f} MB ({pct(shared, total):.0f}%)",
|
||||
]
|
||||
|
||||
if device_states:
|
||||
mem_pcts = [m.deviceState.memoryUsagePercent for m in device_states]
|
||||
lines.append(f" deviceState memory: {np.min(mem_pcts)}-{np.max(mem_pcts)}% (avg {np.mean(mem_pcts):.0f}%)")
|
||||
|
||||
print("\n-- Memory Summary --")
|
||||
print("\n".join(lines))
|
||||
return total
|
||||
|
||||
|
||||
def collect_per_process_mem(proc_logs, use_pss):
|
||||
"""Collect per-process memory samples. Returns {name: {metric: [values_per_sample_in_MB]}}."""
|
||||
by_proc = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
for msg in proc_logs:
|
||||
sample = defaultdict(lambda: defaultdict(float))
|
||||
for proc in msg.procLog.procs:
|
||||
name = get_proc_name(proc)
|
||||
sample[name]['rss'] += proc.memRss / MB
|
||||
if use_pss:
|
||||
sample[name]['pss'] += proc.memPss / MB
|
||||
sample[name]['pss_anon'] += proc.memPssAnon / MB
|
||||
sample[name]['pss_shmem'] += proc.memPssShmem / MB
|
||||
|
||||
for name, metrics in sample.items():
|
||||
for metric, val in metrics.items():
|
||||
by_proc[name][metric].append(val)
|
||||
|
||||
return by_proc
|
||||
|
||||
|
||||
def _has_pss_detail(by_proc) -> bool:
|
||||
"""Check if any process has non-zero pss_anon/pss_shmem (unavailable on some kernels)."""
|
||||
return any(sum(v.get('pss_anon', [])) > 0 or sum(v.get('pss_shmem', [])) > 0 for v in by_proc.values())
|
||||
|
||||
|
||||
def process_table_rows(by_proc, total_mb, use_pss, show_detail):
|
||||
"""Build table rows. Returns (rows, total_row)."""
|
||||
mem_key = 'pss' if use_pss else 'rss'
|
||||
rows = []
|
||||
for name in sorted(by_proc, key=lambda n: np.mean(by_proc[n][mem_key]), reverse=True):
|
||||
m = by_proc[name]
|
||||
vals = m[mem_key]
|
||||
avg = round(np.mean(vals))
|
||||
row = [name, f"{avg} MB", f"{round(np.max(vals))} MB", f"{round(pct(avg, total_mb), 1)}%"]
|
||||
if show_detail:
|
||||
row.append(f"{round(np.mean(m['pss_anon']))} MB")
|
||||
row.append(f"{round(np.mean(m['pss_shmem']))} MB")
|
||||
rows.append(row)
|
||||
|
||||
# Total row
|
||||
total_row = None
|
||||
if by_proc:
|
||||
max_samples = max(len(v[mem_key]) for v in by_proc.values())
|
||||
totals = []
|
||||
for i in range(max_samples):
|
||||
s = sum(v[mem_key][i] for v in by_proc.values() if i < len(v[mem_key]))
|
||||
totals.append(s)
|
||||
avg_total = round(np.mean(totals))
|
||||
total_row = ["TOTAL", f"{avg_total} MB", f"{round(np.max(totals))} MB", f"{round(pct(avg_total, total_mb), 1)}%"]
|
||||
if show_detail:
|
||||
total_row.append(f"{round(sum(np.mean(v['pss_anon']) for v in by_proc.values()))} MB")
|
||||
total_row.append(f"{round(sum(np.mean(v['pss_shmem']) for v in by_proc.values()))} MB")
|
||||
|
||||
return rows, total_row
|
||||
|
||||
|
||||
def print_process_tables(op_procs, other_procs, total_mb, use_pss):
|
||||
all_procs = {**op_procs, **other_procs}
|
||||
show_detail = use_pss and _has_pss_detail(all_procs)
|
||||
|
||||
header = ["process", "avg", "max", "%"]
|
||||
if show_detail:
|
||||
header += ["anon", "shmem"]
|
||||
|
||||
op_rows, op_total = process_table_rows(op_procs, total_mb, use_pss, show_detail)
|
||||
# filter other: >5MB avg and not bare interpreter paths (test infra noise)
|
||||
other_filtered = {n: v for n, v in other_procs.items()
|
||||
if np.mean(v['pss' if use_pss else 'rss']) > 5.0
|
||||
and os.path.basename(n.split()[0]) not in ('python', 'python3')}
|
||||
other_rows, other_total = process_table_rows(other_filtered, total_mb, use_pss, show_detail)
|
||||
|
||||
rows = op_rows
|
||||
if op_total:
|
||||
rows.append(op_total)
|
||||
if other_rows:
|
||||
sep_width = len(header)
|
||||
rows.append([""] * sep_width)
|
||||
rows.extend(other_rows)
|
||||
if other_total:
|
||||
other_total[0] = "TOTAL (other)"
|
||||
rows.append(other_total)
|
||||
|
||||
metric = "PSS (no shared double-count)" if use_pss else "RSS (includes shared, overcounts)"
|
||||
print(f"\n-- Per-Process Memory: {metric} --")
|
||||
print(tabulate(rows, header, **TABULATE_OPTS))
|
||||
|
||||
|
||||
def print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss):
|
||||
last = proc_logs[-1].procLog.mem
|
||||
used = (last.total - last.available) / MB
|
||||
shared = last.shared / MB
|
||||
cached_buf = (last.buffers + last.cached) / MB - shared # shared (MSGQ) is in Cached; separate it
|
||||
msgq = shared
|
||||
|
||||
mem_key = 'pss' if use_pss else 'rss'
|
||||
op_total = sum(v[mem_key][-1] for v in op_procs.values()) if op_procs else 0
|
||||
other_total = sum(v[mem_key][-1] for v in other_procs.values()) if other_procs else 0
|
||||
proc_sum = op_total + other_total
|
||||
remainder = used - (cached_buf + msgq) - proc_sum
|
||||
|
||||
if not use_pss:
|
||||
# RSS double-counts shared; add back once to partially correct
|
||||
remainder += shared
|
||||
|
||||
header = ["", "MB", "%", ""]
|
||||
label = "PSS" if use_pss else "RSS*"
|
||||
rows = [
|
||||
["Used (total - avail)", f"{used:.0f}", f"{pct(used, total_mb):.1f}", "memory in use by the system"],
|
||||
[" Cached + Buffers", f"{cached_buf:.0f}", f"{pct(cached_buf, total_mb):.1f}", "pagecache + fs metadata, reclaimable"],
|
||||
[" MSGQ (shared)", f"{msgq:.0f}", f"{pct(msgq, total_mb):.1f}", "/dev/shm tmpfs, also in process PSS"],
|
||||
[f" openpilot {label}", f"{op_total:.0f}", f"{pct(op_total, total_mb):.1f}", "sum of openpilot process memory"],
|
||||
[f" other {label}", f"{other_total:.0f}", f"{pct(other_total, total_mb):.1f}", "sum of non-openpilot process memory"],
|
||||
[" kernel/ION/GPU", f"{remainder:.0f}", f"{pct(remainder, total_mb):.1f}", "slab, ION/DMA-BUF, GPU, page tables"],
|
||||
]
|
||||
note = "" if use_pss else " (*RSS overcounts shared mem)"
|
||||
print(f"\n-- Memory Accounting (last sample){note} --")
|
||||
print(tabulate(rows, header, tablefmt="simple_grid", stralign="right"))
|
||||
|
||||
|
||||
def print_report(proc_logs, device_states=None):
|
||||
"""Print full memory analysis report. Can be called from tests or CLI."""
|
||||
if not proc_logs:
|
||||
print("No procLog messages found")
|
||||
return
|
||||
|
||||
print(f"{len(proc_logs)} procLog samples, {len(device_states or [])} deviceState samples")
|
||||
|
||||
use_pss = has_pss(proc_logs)
|
||||
if not use_pss:
|
||||
print(" (no PSS data — re-record with updated proclogd for accurate numbers)")
|
||||
|
||||
total_mb = print_summary(proc_logs, device_states or [])
|
||||
|
||||
by_proc = collect_per_process_mem(proc_logs, use_pss)
|
||||
op_procs = {n: v for n, v in by_proc.items() if is_openpilot_proc(n)}
|
||||
other_procs = {n: v for n, v in by_proc.items() if not is_openpilot_proc(n)}
|
||||
|
||||
print_process_tables(op_procs, other_procs, total_mb, use_pss)
|
||||
print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Analyze memory usage from route logs")
|
||||
parser.add_argument("route", nargs="?", default=None, help="route ID or local rlog path")
|
||||
parser.add_argument("--demo", action="store_true", help=f"use demo route ({DEMO_ROUTE})")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.demo:
|
||||
route = DEMO_ROUTE
|
||||
elif args.route:
|
||||
route = args.route
|
||||
else:
|
||||
parser.error("provide a route or use --demo")
|
||||
|
||||
print(f"Reading logs from: {route}")
|
||||
|
||||
proc_logs = []
|
||||
device_states = []
|
||||
for msg in LogReader(route):
|
||||
if msg.which() == 'procLog':
|
||||
proc_logs.append(msg)
|
||||
elif msg.which() == 'deviceState':
|
||||
device_states.append(msg)
|
||||
|
||||
print_report(proc_logs, device_states)
|
||||
@@ -3,14 +3,15 @@ import time
|
||||
|
||||
from cereal import car, log, messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model
|
||||
from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model, is_modeld_sunny
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
|
||||
if __name__ == "__main__":
|
||||
CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10)
|
||||
params = Params()
|
||||
params.put("CarParams", CP.to_bytes())
|
||||
|
||||
if is_modeld_sunny:= is_modeld_sunny(False, params, CP):
|
||||
print("Using sunnypilot custom modeld")
|
||||
if use_snpe_modeld := is_snpe_model(False, params, CP):
|
||||
print("Using SNPE modeld")
|
||||
if use_tinygrad_modeld := is_tinygrad_model(False, params, CP):
|
||||
|
||||
@@ -270,7 +270,7 @@ def main():
|
||||
estimator = LocationEstimator(DEBUG)
|
||||
|
||||
filter_initialized = False
|
||||
critcal_services = ["accelerometer", "gyroscope", "cameraOdometry"]
|
||||
critcal_services = ["accelerometer", "gyroscope"]
|
||||
observation_input_invalid = defaultdict(int)
|
||||
|
||||
input_invalid_limit = {s: round(INPUT_INVALID_LIMIT * (SERVICE_LIST[s].frequency / 20.)) for s in critcal_services}
|
||||
@@ -320,8 +320,7 @@ def main():
|
||||
filter_initialized = sm.all_checks() and sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
|
||||
|
||||
if sm.updated["cameraOdometry"]:
|
||||
critical_service_inputs_valid = all(observation_input_invalid[s] < input_invalid_threshold[s] for s in critcal_services)
|
||||
inputs_valid = sm.all_valid() and critical_service_inputs_valid
|
||||
inputs_valid = True
|
||||
sensors_valid = sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
|
||||
|
||||
msg = estimator.get_msg(sensors_valid, inputs_valid, filter_initialized)
|
||||
|
||||
@@ -50,7 +50,7 @@ def tg_compile(flags, model_name):
|
||||
# Compile small models
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
flags = {
|
||||
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0',
|
||||
'larch64': 'DEV=QCOM',
|
||||
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
|
||||
tg_compile(flags, model_name)
|
||||
|
||||
@@ -59,7 +59,7 @@ class ModelState:
|
||||
self.tensor_inputs['input_img'] = Tensor(self.frame.buffer_from_cl(input_img_cl).reshape(self.input_shapes['input_img']), dtype=dtypes.uint8).realize()
|
||||
|
||||
|
||||
output = self.model_run(**self.tensor_inputs).numpy().flatten()
|
||||
output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
|
||||
t2 = time.perf_counter()
|
||||
return output, t2 - t1
|
||||
|
||||
@@ -217,7 +217,7 @@ class ModelState(ModelStateBase):
|
||||
self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k]
|
||||
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
||||
|
||||
self.policy_output = self.policy_run(**self.policy_inputs).numpy().flatten()
|
||||
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
|
||||
|
||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||
|
||||
Binary file not shown.
@@ -35,14 +35,7 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._EYE_THRESHOLD = 0.65
|
||||
self._SG_THRESHOLD = 0.9
|
||||
self._BLINK_THRESHOLD = 0.865
|
||||
|
||||
self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4
|
||||
self._PHONE_THRESH2 = 15.0
|
||||
self._PHONE_MAX_OFFSET = 0.06
|
||||
self._PHONE_MIN_OFFSET = 0.025
|
||||
self._PHONE_DATA_AVG = 0.05
|
||||
self._PHONE_DATA_VAR = 3*0.005
|
||||
self._PHONE_MAX_COUNT = int(360 / self._DT_DMON)
|
||||
self._PHONE_THRESH = 0.5
|
||||
|
||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||
@@ -152,11 +145,10 @@ class DriverMonitoring:
|
||||
|
||||
# init driver status
|
||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||
phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2)
|
||||
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||
self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT)
|
||||
self.pose = DriverPose(settings=self.settings)
|
||||
self.blink = DriverBlink()
|
||||
self.phone_prob = 0.
|
||||
|
||||
self.always_on = always_on
|
||||
self.distracted_types = []
|
||||
@@ -257,12 +249,7 @@ class DriverMonitoring:
|
||||
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
||||
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
||||
|
||||
if self.phone.prob_calibrated:
|
||||
using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \
|
||||
* self.settings._PHONE_THRESH2
|
||||
else:
|
||||
using_phone = self.phone.prob > self.settings._PHONE_THRESH
|
||||
if using_phone:
|
||||
if self.phone_prob > self.settings._PHONE_THRESH:
|
||||
distracted_types.append(DistractedType.DISTRACTED_PHONE)
|
||||
|
||||
return distracted_types
|
||||
@@ -301,7 +288,7 @@ class DriverMonitoring:
|
||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||
self.phone.prob = driver_data.phoneProb
|
||||
self.phone_prob = driver_data.phoneProb
|
||||
|
||||
self.distracted_types = self._get_distracted_types()
|
||||
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
|
||||
@@ -315,11 +302,9 @@ class DriverMonitoring:
|
||||
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
|
||||
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
|
||||
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
|
||||
self.phone.prob_offseter.push_and_update(self.phone.prob)
|
||||
|
||||
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
|
||||
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||
self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||
|
||||
if self.face_detected and not self.driver_distracted:
|
||||
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
|
||||
@@ -425,8 +410,6 @@ class DriverMonitoring:
|
||||
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
|
||||
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
|
||||
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
|
||||
"phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(),
|
||||
"phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n,
|
||||
"stepChange": self.step_change,
|
||||
"awarenessActive": self.awareness_active,
|
||||
"awarenessPassive": self.awareness_passive,
|
||||
|
||||
@@ -6,16 +6,16 @@ import time
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
from panda import Panda, PandaDFU, PandaProtocolMismatch, McuType, FW_PATH
|
||||
from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
|
||||
def get_expected_signature() -> bytes:
|
||||
def get_expected_signature(panda: Panda) -> bytes:
|
||||
try:
|
||||
fn = os.path.join(FW_PATH, McuType.H7.config.app_fn)
|
||||
fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn)
|
||||
return Panda.get_signature_from_firmware(fn)
|
||||
except Exception:
|
||||
cloudlog.exception("Error computing expected signature")
|
||||
@@ -35,7 +35,7 @@ def flash_panda(panda_serial: str) -> Panda:
|
||||
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
|
||||
return panda
|
||||
|
||||
fw_signature = get_expected_signature()
|
||||
fw_signature = get_expected_signature(panda)
|
||||
internal_panda = panda.is_internal()
|
||||
|
||||
panda_version = "bootstub" if panda.bootstub else panda.get_version()
|
||||
|
||||
@@ -304,6 +304,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.stockLkas: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Stock LKAS: Lane Departure Detected",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.),
|
||||
ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"),
|
||||
},
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP']
|
||||
if SIMULATION:
|
||||
ignore += ['driverCameraState', 'managerState']
|
||||
ignore += ['driverCameraState', 'managerState', 'livePose', 'liveCalibration', 'liveParameters', 'liveTorqueParameters', 'liveDelay', 'driverAssistance']
|
||||
if REPLAY:
|
||||
# no vipc in replay will make them ignored anyways
|
||||
ignore += ['roadCameraState', 'wideRoadCameraState']
|
||||
@@ -388,8 +388,8 @@ class SelfdriveD(CruiseHelper):
|
||||
if not self.CP.notCar:
|
||||
if not self.sm['livePose'].posenetOK:
|
||||
self.events.add(EventName.posenetInvalid)
|
||||
if not self.sm['livePose'].inputsOK:
|
||||
self.events.add(EventName.locationdTemporaryError)
|
||||
# if not self.sm['livePose'].inputsOK:
|
||||
# self.events.add(EventName.locationdTemporaryError)
|
||||
if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
|
||||
self.events.add(EventName.paramsdTemporaryError)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Currently the following processes are tested:
|
||||
### Usage
|
||||
```
|
||||
Usage: test_processes.py [-h] [--whitelist-procs PROCS] [--whitelist-cars CARS] [--blacklist-procs PROCS]
|
||||
[--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs]
|
||||
[--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs] [--upload-only]
|
||||
Regression test to identify changes in a process's output
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -33,6 +33,7 @@ optional arguments:
|
||||
--ignore-fields IGNORE_FIELDS Extra fields or msgs to ignore (e.g. driverMonitoringState.events)
|
||||
--ignore-msgs IGNORE_MSGS Msgs to ignore (e.g. onroadEvents)
|
||||
--update-refs Updates reference logs using current commit
|
||||
--upload-only Skips testing processes and uploads logs from previous test run
|
||||
```
|
||||
|
||||
## Forks
|
||||
|
||||
@@ -9,7 +9,7 @@ from itertools import zip_longest
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from openpilot.common.utils import tabulate
|
||||
from tabulate import tabulate
|
||||
|
||||
from openpilot.common.git import get_commit
|
||||
from openpilot.system.hardware import PC
|
||||
|
||||
1
selfdrive/test/process_replay/ref_commit
Normal file
1
selfdrive/test/process_replay/ref_commit
Normal file
@@ -0,0 +1 @@
|
||||
67f3daf309dc6cbb6844fcbaeb83e6596637e551
|
||||
@@ -9,13 +9,14 @@ from typing import Any
|
||||
|
||||
from opendbc.car.car_helpers import interface_names
|
||||
from openpilot.common.git import get_commit
|
||||
from openpilot.tools.lib.openpilotci import get_url
|
||||
from openpilot.tools.lib.openpilotci import get_url, upload_file
|
||||
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, replay_process, \
|
||||
check_most_messages_valid
|
||||
from openpilot.tools.lib.filereader import FileReader
|
||||
from openpilot.tools.lib.logreader import LogReader, save_log
|
||||
from openpilot.tools.lib.url_file import URLFile
|
||||
|
||||
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
||||
|
||||
source_segments = [
|
||||
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
|
||||
@@ -65,17 +66,46 @@ segments = [
|
||||
# dashcamOnly makes don't need to be tested until a full port is done
|
||||
excluded_interfaces = ["mock", "body", "psa"]
|
||||
|
||||
BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/"
|
||||
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
||||
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
||||
EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
|
||||
|
||||
|
||||
def preserve_only_specified_files_from_ref_commit(*commits_to_keep):
|
||||
"""Keep only files in fakedata that contain any of the specified commit hashes."""
|
||||
removed = 0
|
||||
for f in os.listdir(FAKEDATA):
|
||||
if not any(commit in f for commit in commits_to_keep):
|
||||
os.remove(os.path.join(FAKEDATA, f))
|
||||
removed += 1
|
||||
if removed > 0:
|
||||
print(f"Removed {removed} old files from {FAKEDATA}")
|
||||
|
||||
|
||||
def handle_output_file(cur_log_fn, local):
|
||||
"""Handle the output file based on whether we're using remote or local storage."""
|
||||
assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}"
|
||||
|
||||
if local:
|
||||
os.system(f"git add '{os.path.realpath(cur_log_fn)}'")
|
||||
else:
|
||||
upload_file(cur_log_fn, os.path.basename(cur_log_fn))
|
||||
os.remove(cur_log_fn)
|
||||
|
||||
|
||||
def run_test_process(data):
|
||||
segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data
|
||||
lr = LogReader.from_bytes(lr_dat)
|
||||
res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
|
||||
# save logs so we can update refs
|
||||
save_log(cur_log_fn, log_msgs)
|
||||
res = None
|
||||
if not args.upload_only:
|
||||
lr = LogReader.from_bytes(lr_dat)
|
||||
res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
|
||||
# save logs so we can upload when updating refs
|
||||
save_log(cur_log_fn, log_msgs)
|
||||
|
||||
if args.update_refs or args.upload_only:
|
||||
print(f'Processing: {os.path.basename(cur_log_fn)}')
|
||||
handle_output_file(cur_log_fn, args.local)
|
||||
|
||||
return (segment, cfg.proc_name, res)
|
||||
|
||||
|
||||
@@ -114,6 +144,27 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non
|
||||
return str(e), log_msgs
|
||||
|
||||
|
||||
def finalize_git_updates(cur_commit, ref_commit_fn):
|
||||
"""Finalize git updates and create commit."""
|
||||
try:
|
||||
# Add all new files first
|
||||
os.system(f"git add {os.path.realpath(ref_commit_fn)}")
|
||||
os.system(f"git add {os.path.realpath(FAKEDATA)}/*.zst")
|
||||
|
||||
# Clean up old files - keep only new ref files since they're becoming the reference
|
||||
preserve_only_specified_files_from_ref_commit(cur_commit)
|
||||
|
||||
# Add the deletions to git
|
||||
os.system(f"git add -u {os.path.realpath(FAKEDATA)}")
|
||||
|
||||
# Create the commit
|
||||
commit_msg = f"test_processes: update ref logs to {cur_commit[:7]}"
|
||||
os.system(f'git commit -m "{commit_msg}"')
|
||||
print("Successfully committed reference log updates")
|
||||
except Exception as e:
|
||||
print(f"Failed to commit changes: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
all_cars = {car for car, _ in segments}
|
||||
all_procs = {cfg.proc_name for cfg in CONFIGS if cfg.proc_name not in EXCLUDED_PROCS}
|
||||
@@ -135,6 +186,10 @@ if __name__ == "__main__":
|
||||
help="Msgs to ignore (e.g. carEvents)")
|
||||
parser.add_argument("--update-refs", action="store_true",
|
||||
help="Updates reference logs using current commit")
|
||||
parser.add_argument("--upload-only", action="store_true",
|
||||
help="Skips testing processes and uploads logs from previous test run")
|
||||
parser.add_argument("--local", action="store_true",
|
||||
help="Use local git/ storage instead of remote (Azure for Comma)")
|
||||
parser.add_argument("-j", "--jobs", type=int, default=max(cpu_count - 2, 1),
|
||||
help="Max amount of parallel jobs")
|
||||
args = parser.parse_args()
|
||||
@@ -144,21 +199,33 @@ if __name__ == "__main__":
|
||||
tested_cars = {c.upper() for c in tested_cars}
|
||||
|
||||
full_test = (tested_procs == all_procs) and (tested_cars == all_cars) and all(len(x) == 0 for x in (args.ignore_fields, args.ignore_msgs))
|
||||
upload = args.update_refs or args.upload_only
|
||||
os.makedirs(os.path.dirname(FAKEDATA), exist_ok=True)
|
||||
|
||||
if args.update_refs:
|
||||
if upload:
|
||||
assert full_test, "Need to run full test when updating refs"
|
||||
|
||||
try:
|
||||
with open(REF_COMMIT_FN) as f:
|
||||
ref_commit = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
ref_commit = URLFile(BASE_URL + "ref_commit", cache=False).read().decode().strip()
|
||||
print("Couldn't find reference commit")
|
||||
sys.exit(1)
|
||||
|
||||
cur_commit = get_commit()
|
||||
if not cur_commit:
|
||||
raise Exception("Couldn't get current commit")
|
||||
|
||||
# Could be set as default in args, but wanted to be more explicit on the flow.
|
||||
if upload and not args.local and not IS_AZURE_TOKEN_DEFINED:
|
||||
print("***** Warning: local/git run was used by default since AZURE_TOKEN was NOT found on the env variables! *****")
|
||||
args.local = True
|
||||
|
||||
# Clean up old files before starting
|
||||
if upload and args.local:
|
||||
print("***** Cleaning up old fakedata for local/git tracked refs *****")
|
||||
preserve_only_specified_files_from_ref_commit(cur_commit, ref_commit)
|
||||
|
||||
print(f"***** testing against commit {ref_commit} *****")
|
||||
|
||||
# check to make sure all car brands are tested
|
||||
@@ -168,11 +235,12 @@ if __name__ == "__main__":
|
||||
|
||||
log_paths: defaultdict[str, dict[str, dict[str, str]]] = defaultdict(lambda: defaultdict(dict))
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool:
|
||||
download_segments = [seg for car, seg in segments if car in tested_cars]
|
||||
log_data: dict[str, LogReader] = {}
|
||||
p1 = pool.map(get_log_data, download_segments)
|
||||
for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)):
|
||||
log_data[segment] = lr
|
||||
if not args.upload_only:
|
||||
download_segments = [seg for car, seg in segments if car in tested_cars]
|
||||
log_data: dict[str, LogReader] = {}
|
||||
p1 = pool.map(get_log_data, download_segments)
|
||||
for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)):
|
||||
log_data[segment] = lr
|
||||
|
||||
pool_args: Any = []
|
||||
for car_brand, segment in segments:
|
||||
@@ -187,15 +255,15 @@ if __name__ == "__main__":
|
||||
if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'):
|
||||
continue
|
||||
|
||||
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst".replace("|", "_"))
|
||||
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
|
||||
if args.update_refs: # reference logs will not exist if routes were just regenerated
|
||||
route, seg_num = segment.rsplit("--", 1)
|
||||
ref_log_path = get_url(route, seg_num, "rlog.zst")
|
||||
ref_log_path = get_url(*segment.rsplit("--", 1,), "rlog.zst")
|
||||
else:
|
||||
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst".replace("|", "_"))
|
||||
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst")
|
||||
ref_log_path = ref_log_fn if os.path.exists(ref_log_fn) else BASE_URL + os.path.basename(ref_log_fn)
|
||||
|
||||
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, log_data[segment]))
|
||||
dat = None if args.upload_only else log_data[segment]
|
||||
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, dat))
|
||||
|
||||
log_paths[segment][cfg.proc_name]['ref'] = ref_log_path
|
||||
log_paths[segment][cfg.proc_name]['new'] = cur_log_fn
|
||||
@@ -203,16 +271,19 @@ if __name__ == "__main__":
|
||||
results: Any = defaultdict(dict)
|
||||
p2 = pool.map(run_test_process, pool_args)
|
||||
for (segment, proc, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)):
|
||||
results[segment][proc] = result
|
||||
if not args.upload_only:
|
||||
results[segment][proc] = result
|
||||
|
||||
diff_short, diff_long, failed = format_diff(results, log_paths, ref_commit)
|
||||
if not args.update_refs:
|
||||
if not upload:
|
||||
with open(os.path.join(PROC_REPLAY_DIR, "diff.txt"), "w") as f:
|
||||
f.write(diff_long)
|
||||
print(diff_short)
|
||||
|
||||
if failed:
|
||||
print("TEST FAILED")
|
||||
print("\n\nTo push the new reference logs for this commit run:")
|
||||
print("./test_processes.py --upload-only")
|
||||
else:
|
||||
print("TEST SUCCEEDED")
|
||||
|
||||
@@ -221,4 +292,8 @@ if __name__ == "__main__":
|
||||
f.write(cur_commit)
|
||||
print(f"\n\nUpdated reference logs for commit: {cur_commit}")
|
||||
|
||||
# Only do git operations if we're in local mode
|
||||
if upload and args.local:
|
||||
finalize_git_updates(cur_commit, REF_COMMIT_FN)
|
||||
|
||||
sys.exit(int(failed))
|
||||
|
||||
@@ -8,7 +8,7 @@ import time
|
||||
import numpy as np
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from openpilot.common.utils import tabulate
|
||||
from tabulate import tabulate
|
||||
|
||||
from cereal import log
|
||||
import cereal.messaging as messaging
|
||||
@@ -56,7 +56,7 @@ PROCS = {
|
||||
"selfdrive.ui.soundd": 3.0,
|
||||
"selfdrive.ui.feedback.feedbackd": 1.0,
|
||||
"selfdrive.monitoring.dmonitoringd": 4.0,
|
||||
"system.proclogd": 7.0,
|
||||
"system.proclogd": 3.0,
|
||||
"system.logmessaged": 1.0,
|
||||
"system.tombstoned": 0,
|
||||
"system.journald": 1.0,
|
||||
@@ -282,12 +282,9 @@ class TestOnroad:
|
||||
print("\n------------------------------------------------")
|
||||
print("--------------- Memory Usage -------------------")
|
||||
print("------------------------------------------------")
|
||||
|
||||
from openpilot.selfdrive.debug.mem_usage import print_report
|
||||
print_report(self.msgs['procLog'], self.msgs['deviceState'])
|
||||
|
||||
offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET)
|
||||
mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]]
|
||||
print("Overall memory usage: ", mems)
|
||||
print("MSGQ (/dev/shm/) usage: ", subprocess.check_output(["du", "-hs", "/dev/shm"]).split()[0].decode())
|
||||
|
||||
# check for big leaks. note that memory usage is
|
||||
|
||||
@@ -50,7 +50,7 @@ class MiciMainLayout(Widget):
|
||||
self._alerts_layout,
|
||||
self._home_layout,
|
||||
self._onroad_layout,
|
||||
], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False)
|
||||
], spacing=0, pad_start=0, pad_end=0)
|
||||
self._scroller.set_reset_scroll_at_show(False)
|
||||
|
||||
# Disable scrolling when onroad is interacting with bookmark
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import threading
|
||||
import json
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
@@ -10,7 +11,7 @@ from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide
|
||||
@@ -120,9 +121,6 @@ class PairBigButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png", icon_size=(33, 60))
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
|
||||
def _update_state(self):
|
||||
if ui_state.prime_state.is_paired():
|
||||
self.set_text("paired")
|
||||
@@ -224,7 +222,7 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
|
||||
if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT:
|
||||
self.set_rotate_icon(False)
|
||||
self.set_value("updater failed\nto respond")
|
||||
self.set_value("updater failed to respond")
|
||||
self._state = UpdaterState.IDLE
|
||||
self._hide_value_t = rl.get_time()
|
||||
|
||||
@@ -305,14 +303,30 @@ class DeviceLayoutMici(NavWidget):
|
||||
self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66))
|
||||
self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off"))
|
||||
|
||||
self._load_languages()
|
||||
|
||||
def language_callback():
|
||||
def selected_language_callback():
|
||||
selected_language = dlg.get_selected_option()
|
||||
ui_state.params.put("LanguageSetting", self._languages[selected_language])
|
||||
|
||||
current_language_name = ui_state.params.get("LanguageSetting")
|
||||
current_language = next(name for name, lang in self._languages.items() if lang == current_language_name)
|
||||
|
||||
dlg = BigMultiOptionDialog(list(self._languages), default=current_language, right_btn_callback=selected_language_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
# lang_button = BigButton("change language", "", "icons_mici/settings/device/language.png")
|
||||
# lang_button.set_click_callback(language_callback)
|
||||
|
||||
regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png")
|
||||
regulatory_btn.set_click_callback(self._on_regulatory)
|
||||
|
||||
driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png")
|
||||
driver_cam_btn = BigButton("driver camera preview", "", "icons_mici/settings/device/cameras.png")
|
||||
driver_cam_btn.set_click_callback(self._show_driver_camera)
|
||||
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png")
|
||||
review_training_guide_btn = BigButton("review training guide", "", "icons_mici/settings/device/info.png")
|
||||
review_training_guide_btn.set_click_callback(self._on_review_training_guide)
|
||||
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
@@ -339,7 +353,7 @@ class DeviceLayoutMici(NavWidget):
|
||||
def _on_regulatory(self):
|
||||
if not self._fcc_dialog:
|
||||
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html"))
|
||||
gui_app.set_modal_overlay(self._fcc_dialog)
|
||||
gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None))
|
||||
|
||||
def _offroad_transition(self):
|
||||
self._power_off_btn.set_visible(ui_state.is_offroad())
|
||||
@@ -357,6 +371,10 @@ class DeviceLayoutMici(NavWidget):
|
||||
self._training_guide = TrainingGuide(completed_callback=completed_callback)
|
||||
gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None))
|
||||
|
||||
def _load_languages(self):
|
||||
with open(os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")) as f:
|
||||
self._languages = json.load(f)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
@@ -3,8 +3,8 @@ from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigCircleToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
@@ -39,7 +39,8 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
self._tethering_toggle_btn = BigCircleToggle("icons_mici/tethering_short.png", toggle_callback=tethering_toggle_callback,
|
||||
icon_size=(82, 82), icon_offset=(0, 12))
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
@@ -55,6 +56,9 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton("IP Address", "Not connected")
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
@@ -70,13 +74,8 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
|
||||
|
||||
self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True)
|
||||
self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
wifi_button = BigButton("wi-fi")
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
@@ -91,7 +90,7 @@ class NetworkLayoutMici(NavWidget):
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
self._wifi_button,
|
||||
wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
@@ -100,6 +99,7 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set initial config
|
||||
@@ -158,22 +158,8 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update wi-fi button with ssid and ip address
|
||||
# TODO: make sure we handle hidden ssids
|
||||
connected_network = next((network for network in networks if network.is_connected), None)
|
||||
self._wifi_button.set_text(normalize_ssid(connected_network.ssid) if connected_network is not None else "wi-fi")
|
||||
self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected")
|
||||
if connected_network is not None:
|
||||
strength = WifiIcon.get_strength_icon_idx(connected_network.strength)
|
||||
if strength == 2:
|
||||
strength_icon = self._wifi_full_txt
|
||||
elif strength == 1:
|
||||
strength_icon = self._wifi_medium_txt
|
||||
else:
|
||||
strength_icon = self._wifi_low_txt
|
||||
self._wifi_button.set_icon(strength_icon)
|
||||
else:
|
||||
self._wifi_button.set_icon(self._wifi_slash_txt)
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
|
||||
@@ -50,16 +50,12 @@ class WifiIcon(Widget):
|
||||
def set_scale(self, scale: float):
|
||||
self._scale = scale
|
||||
|
||||
@staticmethod
|
||||
def get_strength_icon_idx(strength: int) -> int:
|
||||
return round(strength / 100 * 2)
|
||||
|
||||
def _render(self, _):
|
||||
if self._network is None:
|
||||
return
|
||||
|
||||
# Determine which wifi strength icon to use
|
||||
strength = self.get_strength_icon_idx(self._network.strength)
|
||||
strength = round(self._network.strength / 100 * 2)
|
||||
if strength == 2:
|
||||
strength_icon = self._wifi_full_txt
|
||||
elif strength == 1:
|
||||
@@ -318,7 +314,7 @@ class WifiUIMici(BigMultiOptionDialog):
|
||||
INACTIVITY_TIMEOUT = 1
|
||||
|
||||
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
|
||||
super().__init__([], None)
|
||||
super().__init__([], None, None, right_btn_callback=None)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
@@ -30,27 +30,22 @@ class PanelInfo:
|
||||
instance: Widget
|
||||
|
||||
|
||||
class SettingsBigButton(BigButton):
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
|
||||
|
||||
class SettingsLayout(NavWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._current_panel = None # PanelType.DEVICE
|
||||
|
||||
toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png")
|
||||
toggles_btn = BigButton("toggles", "", "icons_mici/settings.png")
|
||||
toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES))
|
||||
network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
|
||||
network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
|
||||
network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK))
|
||||
device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
|
||||
device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
|
||||
device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE))
|
||||
developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
|
||||
developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
|
||||
developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER))
|
||||
|
||||
firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
|
||||
firehose_btn = BigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
|
||||
firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE))
|
||||
|
||||
self._scroller = Scroller([
|
||||
|
||||
@@ -14,7 +14,7 @@ from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
||||
from openpilot.common.filter_simple import BounceFilter
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
|
||||
from openpilot.common.transformations.orientation import rot_from_euler
|
||||
from enum import IntEnum
|
||||
@@ -169,7 +169,6 @@ class AugmentedRoadView(CameraView):
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||
self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
|
||||
|
||||
# debug
|
||||
self._pm = messaging.PubMaster(['uiDebug'])
|
||||
@@ -222,11 +221,8 @@ class AugmentedRoadView(CameraView):
|
||||
# Draw all UI overlays
|
||||
self._model_renderer.render(self._content_rect)
|
||||
|
||||
# Fade out bottom of overlays for looks (only when engaged)
|
||||
fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
|
||||
if fade_alpha > 1e-2:
|
||||
rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0,
|
||||
rl.Color(255, 255, 255, int(255 * fade_alpha)))
|
||||
# Fade out bottom of overlays for looks
|
||||
rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
alert_to_render, not_animating_out = self._alert_renderer.will_render()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP, ModelRendererSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
@@ -51,10 +51,9 @@ class LeadVehicle:
|
||||
fill_alpha: int = 0
|
||||
|
||||
|
||||
class ModelRenderer(Widget, ModelRendererSP):
|
||||
class ModelRenderer(Widget):
|
||||
def __init__(self):
|
||||
Widget.__init__(self)
|
||||
ModelRendererSP.__init__(self)
|
||||
super().__init__()
|
||||
self._longitudinal_control = False
|
||||
self._experimental_mode = False
|
||||
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
|
||||
@@ -341,10 +340,6 @@ class ModelRenderer(Widget, ModelRendererSP):
|
||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
||||
self._blend_filter.update(int(allow_throttle))
|
||||
|
||||
if ui_state.rainbow_path:
|
||||
self.rainbow_path.draw_rainbow_path(self._rect, self._path)
|
||||
return
|
||||
|
||||
if self._experimental_mode:
|
||||
# Draw with acceleration coloring
|
||||
if ui_state.status == UIStatus.DISENGAGED:
|
||||
|
||||
@@ -3,8 +3,9 @@ from typing import Union
|
||||
from enum import Enum
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.common.filter_simple import BounceFilter
|
||||
|
||||
@@ -17,7 +18,6 @@ SCROLLING_SPEED_PX_S = 50
|
||||
COMPLICATION_SIZE = 36
|
||||
LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
LABEL_HORIZONTAL_PADDING = 40
|
||||
LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma
|
||||
COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255)
|
||||
PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07
|
||||
|
||||
@@ -52,12 +52,6 @@ class BigCircleButton(Widget):
|
||||
def set_enable_pressed_state(self, pressed: bool):
|
||||
self._press_state_enabled = pressed
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
# draw icon
|
||||
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
|
||||
rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0],
|
||||
btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color)
|
||||
|
||||
def _render(self, _):
|
||||
# draw background
|
||||
txt_bg = self._txt_btn_bg if not self._red else self._txt_btn_red_bg
|
||||
@@ -71,7 +65,10 @@ class BigCircleButton(Widget):
|
||||
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
|
||||
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
|
||||
|
||||
self._draw_content(btn_y)
|
||||
# draw icon
|
||||
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
|
||||
rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0]),
|
||||
int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), icon_color)
|
||||
|
||||
|
||||
class BigCircleToggle(BigCircleButton):
|
||||
@@ -96,41 +93,48 @@ class BigCircleToggle(BigCircleButton):
|
||||
if self._toggle_callback:
|
||||
self._toggle_callback(self._checked)
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
|
||||
# draw status icon
|
||||
rl.draw_texture_ex(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
|
||||
(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2, btn_y + 5),
|
||||
0, 1.0, rl.WHITE)
|
||||
rl.draw_texture(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
|
||||
int(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2),
|
||||
int(self._rect.y + 5), rl.WHITE)
|
||||
|
||||
|
||||
class BigButton(Widget):
|
||||
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
|
||||
|
||||
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64),
|
||||
scroll: bool = False):
|
||||
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64)):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 402, 180))
|
||||
self.text = text
|
||||
self.value = value
|
||||
self._icon_size = icon_size
|
||||
self._scroll = scroll
|
||||
self.set_icon(icon)
|
||||
|
||||
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
|
||||
self._rotate_icon_t: float | None = None
|
||||
|
||||
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
||||
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
|
||||
line_height=0.9)
|
||||
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
||||
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
self._update_label_layout()
|
||||
self._label_font = gui_app.font(FontWeight.DISPLAY)
|
||||
self._value_font = gui_app.font(FontWeight.ROMAN)
|
||||
|
||||
self._label = MiciLabel(text, font_size=self._get_label_font_size(), width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
|
||||
font_weight=FontWeight.DISPLAY, color=LABEL_COLOR,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
|
||||
self._sub_label = MiciLabel(value, font_size=COMPLICATION_SIZE, width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
|
||||
font_weight=FontWeight.ROMAN, color=COMPLICATION_GREY,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
|
||||
|
||||
self._load_images()
|
||||
|
||||
# internal state
|
||||
self._scroll_offset = 0 # in pixels
|
||||
self._needs_scroll = measure_text_cached(self._label_font, text, self._get_label_font_size()).x + 25 > self._rect.width
|
||||
self._scroll_timer = 0
|
||||
self._scroll_state = ScrollState.PRE_SCROLL
|
||||
|
||||
def set_icon(self, icon: Union[str, rl.Texture]):
|
||||
self._txt_icon = gui_app.texture(icon, *self._icon_size) if isinstance(icon, str) and len(icon) else icon
|
||||
|
||||
@@ -145,33 +149,28 @@ class BigButton(Widget):
|
||||
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
|
||||
self._txt_hover_bg = gui_app.texture("icons_mici/buttons/button_rectangle_hover.png", 402, 180)
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
# Single line if scrolling, so hide behind icon if exists
|
||||
icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
|
||||
return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - icon_size)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
if len(self.text) <= 18:
|
||||
return 48
|
||||
if len(self.text) < 12:
|
||||
font_size = 64
|
||||
elif len(self.text) < 17:
|
||||
font_size = 48
|
||||
elif len(self.text) < 20:
|
||||
font_size = 42
|
||||
else:
|
||||
return 42
|
||||
font_size = 36
|
||||
|
||||
def _update_label_layout(self):
|
||||
self._label.set_font_size(self._get_label_font_size())
|
||||
if self.value:
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
else:
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
font_size -= 20
|
||||
|
||||
return font_size
|
||||
|
||||
def set_text(self, text: str):
|
||||
self.text = text
|
||||
self._label.set_text(text)
|
||||
self._update_label_layout()
|
||||
|
||||
def set_value(self, value: str):
|
||||
self.value = value
|
||||
self._sub_label.set_text(value)
|
||||
self._update_label_layout()
|
||||
|
||||
def get_value(self) -> str:
|
||||
return self.value
|
||||
@@ -179,35 +178,37 @@ class BigButton(Widget):
|
||||
def get_text(self):
|
||||
return self.text
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
# LABEL ------------------------------------------------------------------
|
||||
label_x = self._rect.x + LABEL_HORIZONTAL_PADDING
|
||||
def _update_state(self):
|
||||
# hold on text for a bit, scroll, hold again, reset
|
||||
if self._needs_scroll:
|
||||
"""`dt` should be seconds since last frame (rl.get_frame_time())."""
|
||||
# TODO: this comment is generated by GPT, prob wrong and misused
|
||||
dt = rl.get_frame_time()
|
||||
|
||||
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
|
||||
self._label.set_color(label_color)
|
||||
label_rect = rl.Rectangle(label_x, btn_y + LABEL_VERTICAL_PADDING, self._width_hint(),
|
||||
self._rect.height - LABEL_VERTICAL_PADDING * 2)
|
||||
self._label.render(label_rect)
|
||||
self._scroll_timer += dt
|
||||
if self._scroll_state == ScrollState.PRE_SCROLL:
|
||||
if self._scroll_timer < 0.5:
|
||||
return
|
||||
self._scroll_state = ScrollState.SCROLLING
|
||||
self._scroll_timer = 0
|
||||
|
||||
if self.value:
|
||||
label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING
|
||||
sub_label_height = self._sub_label.get_content_height(self._width_hint())
|
||||
sub_label_rect = rl.Rectangle(label_x, label_y - sub_label_height, self._width_hint(), sub_label_height)
|
||||
self._sub_label.render(sub_label_rect)
|
||||
elif self._scroll_state == ScrollState.SCROLLING:
|
||||
self._scroll_offset -= SCROLLING_SPEED_PX_S * dt
|
||||
# reset when text has completely left the button + 50 px gap
|
||||
# TODO: use global constant for 30+30 px gap
|
||||
# TODO: add std Widget padding option integrated into the self._rect
|
||||
full_len = measure_text_cached(self._label_font, self.text, self._get_label_font_size()).x + 30 + 30
|
||||
if self._scroll_offset < (self._rect.width - full_len):
|
||||
self._scroll_state = ScrollState.POST_SCROLL
|
||||
self._scroll_timer = 0
|
||||
|
||||
# ICON -------------------------------------------------------------------
|
||||
if self._txt_icon:
|
||||
rotation = 0
|
||||
if self._rotate_icon_t is not None:
|
||||
rotation = (rl.get_time() - self._rotate_icon_t) * 180
|
||||
|
||||
# draw top right with 30px padding
|
||||
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
|
||||
y = btn_y + 30 + self._txt_icon.height / 2
|
||||
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
|
||||
dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height)
|
||||
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
|
||||
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
|
||||
elif self._scroll_state == ScrollState.POST_SCROLL:
|
||||
# wait for a bit before starting to scroll again
|
||||
if self._scroll_timer < 0.75:
|
||||
return
|
||||
self._scroll_state = ScrollState.PRE_SCROLL
|
||||
self._scroll_timer = 0
|
||||
self._scroll_offset = 0
|
||||
|
||||
def _render(self, _):
|
||||
# draw _txt_default_bg
|
||||
@@ -222,7 +223,33 @@ class BigButton(Widget):
|
||||
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
|
||||
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
|
||||
|
||||
self._draw_content(btn_y)
|
||||
# LABEL ------------------------------------------------------------------
|
||||
lx = self._rect.x + LABEL_HORIZONTAL_PADDING
|
||||
ly = btn_y + self._rect.height - 33 # - 40# - self._get_label_font_size() / 2
|
||||
|
||||
if self.value:
|
||||
self._sub_label.set_position(lx, ly)
|
||||
ly -= self._sub_label.font_size + 9
|
||||
self._sub_label.render()
|
||||
|
||||
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
|
||||
self._label.set_color(label_color)
|
||||
self._label.set_position(lx, ly)
|
||||
self._label.render()
|
||||
|
||||
# ICON -------------------------------------------------------------------
|
||||
if self._txt_icon:
|
||||
rotation = 0
|
||||
if self._rotate_icon_t is not None:
|
||||
rotation = (rl.get_time() - self._rotate_icon_t) * 180
|
||||
|
||||
# drop top right with 30px padding
|
||||
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
|
||||
y = self._rect.y + 30 + self._txt_icon.height / 2
|
||||
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
|
||||
dest_rec = rl.Rectangle(int(x), int(y), self._txt_icon.width, self._txt_icon.height)
|
||||
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
|
||||
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
|
||||
|
||||
|
||||
class BigToggle(BigButton):
|
||||
@@ -231,6 +258,8 @@ class BigToggle(BigButton):
|
||||
self._checked = initial_state
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
self._label.set_font_size(48)
|
||||
|
||||
def _load_images(self):
|
||||
super()._load_images()
|
||||
self._txt_enabled_toggle = gui_app.texture("icons_mici/buttons/toggle_pill_enabled.png", 84, 66)
|
||||
@@ -248,15 +277,15 @@ class BigToggle(BigButton):
|
||||
def _draw_pill(self, x: float, y: float, checked: bool):
|
||||
# draw toggle icon top right
|
||||
if checked:
|
||||
rl.draw_texture_ex(self._txt_enabled_toggle, (x, y), 0, 1.0, rl.WHITE)
|
||||
rl.draw_texture(self._txt_enabled_toggle, int(x), int(y), rl.WHITE)
|
||||
else:
|
||||
rl.draw_texture_ex(self._txt_disabled_toggle, (x, y), 0, 1.0, rl.WHITE)
|
||||
rl.draw_texture(self._txt_disabled_toggle, int(x), int(y), rl.WHITE)
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
|
||||
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
|
||||
y = btn_y
|
||||
y = self._rect.y
|
||||
self._draw_pill(x, y, self._checked)
|
||||
|
||||
|
||||
@@ -268,10 +297,15 @@ class BigMultiToggle(BigToggle):
|
||||
self._options = options
|
||||
self._select_callback = select_callback
|
||||
|
||||
self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width))
|
||||
# TODO: why isn't this automatic?
|
||||
self._label.set_font_size(self._get_label_font_size())
|
||||
|
||||
self.set_value(self._options[0])
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)
|
||||
def _get_label_font_size(self):
|
||||
font_size = super()._get_label_font_size()
|
||||
return font_size - 6
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
@@ -281,14 +315,13 @@ class BigMultiToggle(BigToggle):
|
||||
if self._select_callback:
|
||||
self._select_callback(self.value)
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
# don't draw pill from BigToggle
|
||||
BigButton._draw_content(self, btn_y)
|
||||
def _render(self, _):
|
||||
BigButton._render(self, _)
|
||||
|
||||
checked_idx = self._options.index(self.value)
|
||||
|
||||
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
|
||||
y = btn_y
|
||||
y = self._rect.y
|
||||
|
||||
for i in range(len(self._options)):
|
||||
self._draw_pill(x, y, checked_idx == i)
|
||||
|
||||
@@ -14,6 +14,7 @@ from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
|
||||
|
||||
DEBUG = False
|
||||
|
||||
@@ -21,17 +22,32 @@ PADDING = 20
|
||||
|
||||
|
||||
class BigDialogBase(NavWidget, abc.ABC):
|
||||
def __init__(self):
|
||||
def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None):
|
||||
super().__init__()
|
||||
self._ret = DialogResult.NO_ACTION
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL))
|
||||
|
||||
self._right_btn = None
|
||||
if right_btn:
|
||||
def right_btn_callback_wrapper():
|
||||
gui_app.set_modal_overlay(None)
|
||||
if right_btn_callback:
|
||||
right_btn_callback()
|
||||
|
||||
self._right_btn = SideButton(right_btn)
|
||||
self._right_btn.set_click_callback(right_btn_callback_wrapper)
|
||||
# move to right side
|
||||
self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width
|
||||
|
||||
def _render(self, _) -> DialogResult:
|
||||
"""
|
||||
Allows `gui_app.set_modal_overlay(BigDialog(...))`.
|
||||
The overlay runner keeps calling until result != NO_ACTION.
|
||||
"""
|
||||
if self._right_btn:
|
||||
self._right_btn.set_position(self._right_btn._rect.x, self._rect.y)
|
||||
self._right_btn.render()
|
||||
|
||||
return self._ret
|
||||
|
||||
@@ -39,8 +55,10 @@ class BigDialogBase(NavWidget, abc.ABC):
|
||||
class BigDialog(BigDialogBase):
|
||||
def __init__(self,
|
||||
title: str,
|
||||
description: str):
|
||||
super().__init__()
|
||||
description: str,
|
||||
right_btn: str | None = None,
|
||||
right_btn_callback: Callable | None = None):
|
||||
super().__init__(right_btn, right_btn_callback)
|
||||
self._title = title
|
||||
self._description = description
|
||||
|
||||
@@ -52,6 +70,8 @@ class BigDialog(BigDialogBase):
|
||||
# TODO: coming up with these numbers manually is a pain and not scalable
|
||||
# TODO: no clue what any of these numbers mean. VBox and HBox would remove all of this shite
|
||||
max_width = self._rect.width - PADDING * 2
|
||||
if self._right_btn:
|
||||
max_width -= self._right_btn._rect.width
|
||||
|
||||
title_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.BOLD), self._title, 50, int(max_width)))
|
||||
title_size = measure_text_cached(gui_app.font(FontWeight.BOLD), title_wrapped, 50)
|
||||
@@ -119,7 +139,7 @@ class BigInputDialog(BigDialogBase):
|
||||
default_text: str = "",
|
||||
minimum_length: int = 1,
|
||||
confirm_callback: Callable[[str], None] | None = None):
|
||||
super().__init__()
|
||||
super().__init__(None, None)
|
||||
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
|
||||
font_weight=FontWeight.MEDIUM)
|
||||
self._keyboard = MiciKeyboard()
|
||||
@@ -131,8 +151,7 @@ class BigInputDialog(BigDialogBase):
|
||||
self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36)
|
||||
self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 76, 62)
|
||||
self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 76, 62)
|
||||
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 42, 36)
|
||||
self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
# rects for top buttons
|
||||
@@ -167,9 +186,9 @@ class BigInputDialog(BigDialogBase):
|
||||
text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, self.TEXT_INPUT_SIZE)
|
||||
|
||||
bg_block_margin = 5
|
||||
text_x = PADDING / 2 + self._enter_img.width + PADDING
|
||||
text_x = PADDING * 2 + self._enter_img.width + bg_block_margin
|
||||
text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin,
|
||||
int(self._rect.width - text_x * 2),
|
||||
int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width) - bg_block_margin * 2,
|
||||
int(text_size.y))
|
||||
|
||||
# draw text input
|
||||
@@ -205,7 +224,7 @@ class BigInputDialog(BigDialogBase):
|
||||
self._backspace_img_alpha.update(255 * bool(text))
|
||||
if self._backspace_img_alpha.x > 1:
|
||||
color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x))
|
||||
rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(self._rect.y + 14), color)
|
||||
rl.draw_texture(self._backspace_img, int(self._rect.width - self._enter_img.width - 15), int(text_field_rect.y), color)
|
||||
|
||||
if not text and self._hint_label.text and not candidate_char:
|
||||
# draw description if no text entered yet and not drawing candidate char
|
||||
@@ -217,12 +236,10 @@ class BigInputDialog(BigDialogBase):
|
||||
self._top_right_button_rect = rl.Rectangle(text_field_rect.x + text_field_rect.width, self._rect.y,
|
||||
self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height)
|
||||
|
||||
# draw enter button
|
||||
self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0)
|
||||
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
|
||||
rl.draw_texture(self._enter_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
|
||||
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
|
||||
rl.draw_texture(self._enter_disabled_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
|
||||
self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 255 * 0.35)
|
||||
if self._enter_img_alpha.x > 1:
|
||||
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
|
||||
rl.draw_texture(self._enter_img, int(self._rect.x + 15), int(text_field_rect.y), color)
|
||||
|
||||
# keyboard goes over everything
|
||||
self._keyboard.render(self._rect)
|
||||
@@ -290,8 +307,9 @@ class BigDialogOptionButton(Widget):
|
||||
class BigMultiOptionDialog(BigDialogBase):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
|
||||
def __init__(self, options: list[str], default: str | None):
|
||||
super().__init__()
|
||||
def __init__(self, options: list[str], default: str | None,
|
||||
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] | None = None):
|
||||
super().__init__(right_btn, right_btn_callback=right_btn_callback)
|
||||
self._options = options
|
||||
if default is not None:
|
||||
assert default in options
|
||||
@@ -304,6 +322,8 @@ class BigMultiOptionDialog(BigDialogBase):
|
||||
self._can_click = True
|
||||
|
||||
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True)
|
||||
if self._right_btn is not None:
|
||||
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
|
||||
|
||||
for option in options:
|
||||
self._scroller.add_widget(BigDialogOptionButton(option))
|
||||
|
||||
31
selfdrive/ui/mici/widgets/side_button.py
Normal file
31
selfdrive/ui/mici/widgets/side_button.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants extracted from the original Qt style
|
||||
# ---------------------------------------------------------------------------
|
||||
# TODO: this should be corrected, but Scroller relies on this being incorrect :/
|
||||
WIDTH, HEIGHT = 112, 240
|
||||
|
||||
|
||||
class SideButton(Widget):
|
||||
def __init__(self, btn_type: str):
|
||||
super().__init__()
|
||||
self.type = btn_type
|
||||
self.set_rect(rl.Rectangle(0, 0, WIDTH, HEIGHT))
|
||||
|
||||
# load pre-rendered button images
|
||||
if btn_type not in ("check", "back"):
|
||||
btn_type = "back"
|
||||
btn_img_path = f"icons_mici/buttons/button_side_{btn_type}.png"
|
||||
btn_img_pressed_path = f"icons_mici/buttons/button_side_{btn_type}_pressed.png"
|
||||
self._txt_btn, self._txt_btn_back = gui_app.texture(btn_img_path, 100, 224), gui_app.texture(btn_img_pressed_path, 100, 224)
|
||||
|
||||
def _render(self, _) -> bool:
|
||||
x = int(self._rect.x + 12)
|
||||
y = int(self._rect.y + (self._rect.height - self._txt_btn.height) / 2)
|
||||
rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back,
|
||||
x, y, rl.WHITE)
|
||||
|
||||
return False
|
||||
@@ -4,190 +4,27 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from 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.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
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
|
||||
class PanelType(IntEnum):
|
||||
CRUISE = 0
|
||||
SLA = 1
|
||||
|
||||
|
||||
ICBM_DESC = tr_noop("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons " +
|
||||
"by emulating button presses for limited longitudinal control.")
|
||||
ICMB_UNAVAILABLE = tr_noop("Intelligent Cruise Button Management is currently unavailable on this platform.")
|
||||
ICMB_UNAVAILABLE_LONG_AVAILABLE = tr_noop("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.")
|
||||
ICMB_UNAVAILABLE_LONG_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control is the default longitudinal control for this platform.")
|
||||
|
||||
ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments for cruise speed increase/decrease.")
|
||||
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.")
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class CruiseLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._current_panel = PanelType.CRUISE
|
||||
self._speed_limit_layout = SpeedLimitSettingsLayout(lambda: self._set_current_panel(PanelType.CRUISE))
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
|
||||
self.icbm_toggle = toggle_item_sp(
|
||||
title=tr("Intelligent Cruise Button Management (ICBM) (Alpha)"),
|
||||
description="",
|
||||
param="IntelligentCruiseButtonManagement")
|
||||
|
||||
self.scc_v_toggle = toggle_item_sp(
|
||||
title=tr("Smart Cruise Control - Vision"),
|
||||
description=tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."),
|
||||
param="SmartCruiseControlVision")
|
||||
|
||||
self.scc_m_toggle = toggle_item_sp(
|
||||
title=tr("Smart Cruise Control - Map"),
|
||||
description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."),
|
||||
param="SmartCruiseControlMap")
|
||||
|
||||
self.custom_acc_toggle = toggle_item_sp(
|
||||
title=tr("Custom ACC Speed Increments"),
|
||||
description="",
|
||||
param="CustomAccIncrementsEnabled",
|
||||
callback=self._on_custom_acc_toggle)
|
||||
|
||||
self.custom_acc_short_increment = option_item_sp(
|
||||
title=tr("Short Press Increment"),
|
||||
param="CustomAccShortPressIncrement",
|
||||
min_value=1, max_value=10, value_change_step=1,
|
||||
inline=True)
|
||||
|
||||
self.custom_acc_long_increment = option_item_sp(
|
||||
title=tr("Long Press Increment"),
|
||||
param="CustomAccLongPressIncrement",
|
||||
value_map={1: 1, 2: 5, 3: 10},
|
||||
min_value=1, max_value=3, value_change_step=1,
|
||||
inline=True)
|
||||
|
||||
self.sla_settings_button = simple_button_item_sp(
|
||||
button_text=lambda: tr("Speed Limit"),
|
||||
button_width=800,
|
||||
callback=lambda: self._set_current_panel(PanelType.SLA)
|
||||
)
|
||||
|
||||
self.dec_toggle = toggle_item_sp(
|
||||
title=tr("Enable Dynamic Experimental Control"),
|
||||
description=tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."),
|
||||
param="DynamicExperimentalControl")
|
||||
|
||||
items = [
|
||||
self.icbm_toggle,
|
||||
self.dec_toggle,
|
||||
self.scc_v_toggle,
|
||||
self.scc_m_toggle,
|
||||
self.custom_acc_toggle,
|
||||
self.custom_acc_short_increment,
|
||||
self.custom_acc_long_increment,
|
||||
self.sla_settings_button,
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
if self._current_panel == PanelType.SLA:
|
||||
self._speed_limit_layout.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._set_current_panel(PanelType.CRUISE)
|
||||
self._scroller.show_event()
|
||||
self.icbm_toggle.show_description(True)
|
||||
self.custom_acc_toggle.show_description(True)
|
||||
|
||||
def _set_current_panel(self, panel: PanelType):
|
||||
self._current_panel = panel
|
||||
if panel == PanelType.SLA:
|
||||
self._speed_limit_layout.show_event()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
if ui_state.CP is not None and ui_state.CP_SP is not None:
|
||||
has_icbm = ui_state.has_icbm
|
||||
has_long = ui_state.has_longitudinal_control
|
||||
|
||||
if ui_state.CP_SP.intelligentCruiseButtonManagementAvailable and not has_long:
|
||||
self.icbm_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
self.icbm_toggle.set_description(tr(ICBM_DESC))
|
||||
else:
|
||||
ui_state.params.remove("IntelligentCruiseButtonManagement")
|
||||
self.icbm_toggle.action_item.set_enabled(False)
|
||||
|
||||
long_desc = ICMB_UNAVAILABLE
|
||||
if has_long:
|
||||
if ui_state.CP.alphaLongitudinalAvailable:
|
||||
long_desc += " " + ICMB_UNAVAILABLE_LONG_AVAILABLE
|
||||
else:
|
||||
long_desc += " " + ICMB_UNAVAILABLE_LONG_UNAVAILABLE
|
||||
|
||||
new_desc = "<b>" + tr(long_desc) + "</b>\n\n" + tr(ICBM_DESC)
|
||||
if self.icbm_toggle.description != new_desc:
|
||||
self.icbm_toggle.set_description(new_desc)
|
||||
self.icbm_toggle.show_description(True)
|
||||
|
||||
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.dec_toggle.action_item.set_enabled(has_long)
|
||||
self.scc_v_toggle.action_item.set_enabled(True)
|
||||
self.scc_m_toggle.action_item.set_enabled(True)
|
||||
else:
|
||||
ui_state.params.remove("CustomAccIncrementsEnabled")
|
||||
ui_state.params.remove("DynamicExperimentalControl")
|
||||
ui_state.params.remove("SmartCruiseControlVision")
|
||||
ui_state.params.remove("SmartCruiseControlMap")
|
||||
self.custom_acc_toggle.action_item.set_enabled(False)
|
||||
self.dec_toggle.action_item.set_enabled(False)
|
||||
self.scc_v_toggle.action_item.set_enabled(False)
|
||||
self.scc_m_toggle.action_item.set_enabled(False)
|
||||
|
||||
else:
|
||||
has_icbm = has_long = False
|
||||
self.icbm_toggle.action_item.set_enabled(False)
|
||||
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
|
||||
|
||||
show_custom_acc_desc = False
|
||||
|
||||
if ui_state.is_offroad():
|
||||
new_custom_acc_desc = tr(ONROAD_ONLY_DESCRIPTION)
|
||||
show_custom_acc_desc = True
|
||||
else:
|
||||
if has_long or has_icbm:
|
||||
if has_long and ui_state.CP.pcmCruise:
|
||||
new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION)
|
||||
show_custom_acc_desc = True
|
||||
else:
|
||||
new_custom_acc_desc = tr(ACC_ENABLED_DESCRIPTION)
|
||||
else:
|
||||
new_custom_acc_desc = tr(ACC_NOLONG_DESCRIPTION)
|
||||
show_custom_acc_desc = True
|
||||
self.custom_acc_toggle.action_item.set_state(False)
|
||||
|
||||
if self.custom_acc_toggle.description != new_custom_acc_desc:
|
||||
self.custom_acc_toggle.set_description(new_custom_acc_desc)
|
||||
if show_custom_acc_desc:
|
||||
self.custom_acc_toggle.show_description(True)
|
||||
|
||||
self._on_custom_acc_toggle(self.custom_acc_toggle.action_item.get_state())
|
||||
|
||||
def _on_custom_acc_toggle(self, state):
|
||||
self.custom_acc_short_increment.set_visible(state)
|
||||
self.custom_acc_long_increment.set_visible(state)
|
||||
self.custom_acc_short_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled)
|
||||
self.custom_acc_long_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
|
||||
from openpilot.system.ui.widgets.network import NavButton
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description
|
||||
|
||||
SPEED_LIMIT_POLICY_BUTTONS = [tr("Car Only"), tr("Map Only"), tr("Car First"), tr("Map First"), tr("Combined")]
|
||||
|
||||
SPEED_LIMIT_POLICY_DESCRIPTIONS = [
|
||||
tr("Car Only: Use Speed Limit data only from Car"),
|
||||
tr("Map Only: Use Speed Limit data only from OpenStreetMaps"),
|
||||
tr("Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps"),
|
||||
tr("Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car"),
|
||||
tr("Combined: Use combined Speed Limit data from Car & OpenStreetMaps")
|
||||
]
|
||||
|
||||
|
||||
class SpeedLimitPolicyLayout(Widget):
|
||||
def __init__(self, back_btn_callback: Callable):
|
||||
super().__init__()
|
||||
self._back_button = NavButton(tr("Back"))
|
||||
self._back_button.set_click_callback(back_btn_callback)
|
||||
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=False, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
self._speed_limit_policy = multiple_button_item_sp(
|
||||
title=lambda: tr("Speed Limit Source"),
|
||||
description=self._get_policy_description,
|
||||
buttons=SPEED_LIMIT_POLICY_BUTTONS,
|
||||
param="SpeedLimitPolicy",
|
||||
button_width=250,
|
||||
)
|
||||
|
||||
items = [
|
||||
self._speed_limit_policy
|
||||
]
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _get_policy_description():
|
||||
return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS)
|
||||
|
||||
def _render(self, rect):
|
||||
self._back_button.set_position(self._rect.x, self._rect.y + 20)
|
||||
self._back_button.render()
|
||||
|
||||
content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40)
|
||||
self._scroller.render(content_rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
self._speed_limit_policy.show_description(True)
|
||||
@@ -1,178 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_policy import SpeedLimitPolicyLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import OffsetType as SpeedLimitOffsetType
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, option_item_sp, simple_button_item_sp, LineSeparatorSP
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.network import NavButton
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")]
|
||||
SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")]
|
||||
|
||||
SPEED_LIMIT_MODE_DESCRIPTIONS = [
|
||||
tr("Off: Disables the Speed Limit functions."),
|
||||
tr("Information: Displays the current road's speed limit."),
|
||||
tr("Warning: Provides a warning when exceeding the current road's speed limit."),
|
||||
tr("Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."),
|
||||
]
|
||||
|
||||
SPEED_LIMIT_OFFSET_DESCRIPTIONS = [
|
||||
tr("None: No Offset"),
|
||||
tr("Fixed: Adds a fixed offset [Speed Limit + Offset]"),
|
||||
tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"),
|
||||
]
|
||||
|
||||
|
||||
class PanelType(IntEnum):
|
||||
SETTINGS = 0
|
||||
POLICY = 1
|
||||
|
||||
|
||||
class SpeedLimitSettingsLayout(Widget):
|
||||
def __init__(self, back_btn_callback: Callable):
|
||||
super().__init__()
|
||||
self._current_panel = PanelType.SETTINGS
|
||||
|
||||
self._back_button = NavButton(tr("Back"))
|
||||
self._back_button.set_click_callback(back_btn_callback)
|
||||
|
||||
self._policy_layout = SpeedLimitPolicyLayout(lambda: self._set_current_panel(PanelType.SETTINGS))
|
||||
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=False, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
self._speed_limit_mode = multiple_button_item_sp(
|
||||
title=lambda: tr("Speed Limit"),
|
||||
description=self._get_mode_description,
|
||||
buttons=SPEED_LIMIT_MODE_BUTTONS,
|
||||
param="SpeedLimitMode",
|
||||
button_width=380,
|
||||
)
|
||||
|
||||
self._source_button = simple_button_item_sp(
|
||||
button_text=lambda: tr("Customize Source"),
|
||||
button_width=720,
|
||||
callback=lambda: self._set_current_panel(PanelType.POLICY)
|
||||
)
|
||||
|
||||
self._speed_limit_offset_type = multiple_button_item_sp(
|
||||
title=lambda: tr("Speed Limit Offset"),
|
||||
description="",
|
||||
buttons=SPEED_LIMIT_OFFSET_TYPE_BUTTONS,
|
||||
param="SpeedLimitOffsetType",
|
||||
button_width=450,
|
||||
)
|
||||
|
||||
self._speed_limit_value_offset = option_item_sp(
|
||||
title="",
|
||||
param="SpeedLimitValueOffset",
|
||||
min_value=-30,
|
||||
max_value=30,
|
||||
description=self._get_offset_description,
|
||||
label_callback=self._get_offset_label,
|
||||
)
|
||||
|
||||
items = [
|
||||
self._speed_limit_mode,
|
||||
LineSeparatorSP(40),
|
||||
self._source_button,
|
||||
LineSeparatorSP(40),
|
||||
self._speed_limit_offset_type,
|
||||
self._speed_limit_value_offset
|
||||
]
|
||||
return items
|
||||
|
||||
def _set_current_panel(self, panel: PanelType):
|
||||
self._current_panel = panel
|
||||
if panel == PanelType.POLICY:
|
||||
self._policy_layout.show_event()
|
||||
|
||||
@staticmethod
|
||||
def _get_mode_description():
|
||||
return get_highlighted_description(ui_state.params, "SpeedLimitMode", SPEED_LIMIT_MODE_DESCRIPTIONS)
|
||||
|
||||
@staticmethod
|
||||
def _get_offset_description():
|
||||
return get_highlighted_description(ui_state.params, "SpeedLimitOffsetType", SPEED_LIMIT_OFFSET_DESCRIPTIONS)
|
||||
|
||||
@staticmethod
|
||||
def _get_offset_label(value):
|
||||
offset_type = int(ui_state.params.get("SpeedLimitOffsetType", return_default=True))
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
|
||||
if offset_type == int(SpeedLimitOffsetType.percentage):
|
||||
return f"{value}%"
|
||||
elif offset_type == int(SpeedLimitOffsetType.fixed):
|
||||
return f"{value} {unit}"
|
||||
return str(value)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
speed_limit_mode_param = ui_state.params.get("SpeedLimitMode", return_default=True)
|
||||
if ui_state.CP is not None and ui_state.CP_SP is not None:
|
||||
brand = ui_state.CP.brand
|
||||
has_long = ui_state.has_longitudinal_control
|
||||
has_icbm = ui_state.has_icbm
|
||||
|
||||
"""
|
||||
Speed Limit Assist is available when:
|
||||
- has_long or has_icbm, and
|
||||
- is not a release branch or not a disallowed brand, and
|
||||
- is not always disallwed
|
||||
"""
|
||||
sla_disallow_in_release = brand == "tesla" and ui_state.is_sp_release
|
||||
sla_always_disallow = brand == "rivian"
|
||||
sla_available = (has_long or has_icbm) and not sla_disallow_in_release and not sla_always_disallow
|
||||
|
||||
if not sla_available and speed_limit_mode_param == int(SpeedLimitMode.assist):
|
||||
ui_state.params.put("SpeedLimitMode", int(SpeedLimitMode.warning))
|
||||
|
||||
else:
|
||||
sla_available = False
|
||||
|
||||
if not sla_available:
|
||||
self._speed_limit_mode.action_item.set_enabled_buttons({
|
||||
int(SpeedLimitMode.off),
|
||||
int(SpeedLimitMode.information),
|
||||
int(SpeedLimitMode.warning),
|
||||
})
|
||||
else:
|
||||
self._speed_limit_mode.action_item.set_enabled_buttons(None)
|
||||
|
||||
offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True)
|
||||
self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off))
|
||||
|
||||
def _render(self, rect):
|
||||
if self._current_panel == PanelType.POLICY:
|
||||
self._policy_layout.render(rect)
|
||||
return
|
||||
|
||||
self._back_button.set_position(self._rect.x, self._rect.y + 20)
|
||||
self._back_button.render()
|
||||
|
||||
content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40)
|
||||
self._scroller.render(content_rect)
|
||||
|
||||
def show_event(self):
|
||||
self._current_panel = PanelType.SETTINGS
|
||||
self._scroller.show_event()
|
||||
self._speed_limit_mode.show_description(True)
|
||||
|
||||
def hide_event(self):
|
||||
self._current_panel = PanelType.SETTINGS
|
||||
self._scroller.hide_event()
|
||||
@@ -72,15 +72,6 @@ class SteeringLayout(Widget):
|
||||
description="",
|
||||
label_callback=lambda speed: f'{speed} {"km/h" if ui_state.is_metric else "mph"}',
|
||||
)
|
||||
self._blinker_reengage_delay = option_item_sp(
|
||||
param="BlinkerLateralReengageDelay",
|
||||
title=lambda: tr("Post-Blinker Delay"),
|
||||
min_value=0,
|
||||
max_value=10,
|
||||
value_change_step=1,
|
||||
description=lambda: tr("Delay before lateral control resumes after the turn signal ends."),
|
||||
label_callback=lambda delay: f'{delay} {"s"}'
|
||||
)
|
||||
self._torque_control_toggle = toggle_item_sp(
|
||||
param="EnforceTorqueControl",
|
||||
title=lambda: tr("Enforce Torque Lateral Control"),
|
||||
@@ -105,7 +96,6 @@ class SteeringLayout(Widget):
|
||||
LineSeparatorSP(40),
|
||||
self._blinker_control_toggle,
|
||||
self._blinker_control_options,
|
||||
self._blinker_reengage_delay,
|
||||
LineSeparatorSP(40),
|
||||
self._torque_control_toggle,
|
||||
self._torque_customization_button,
|
||||
@@ -138,7 +128,6 @@ class SteeringLayout(Widget):
|
||||
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())
|
||||
self._blinker_control_options.set_visible(self._blinker_control_toggle.action_item.get_state())
|
||||
self._blinker_reengage_delay.set_visible(self._blinker_control_toggle.action_item.get_state())
|
||||
|
||||
enforce_torque_enabled = self._torque_control_toggle.action_item.get_state()
|
||||
nnlc_enabled = self._nnlc_toggle.action_item.get_state()
|
||||
|
||||
@@ -9,7 +9,6 @@ import pyray as rl
|
||||
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.network import NavButton
|
||||
@@ -113,7 +112,7 @@ class MadsSettingsLayout(Widget):
|
||||
if self._mads_limited_settings():
|
||||
ui_state.params.remove("MadsMainCruiseAllowed")
|
||||
ui_state.params.put_bool("MadsUnifiedEngagementMode", True)
|
||||
ui_state.params.put("MadsSteeringMode", MadsSteeringModeOnBrake.DISENGAGE)
|
||||
ui_state.params.put("MadsSteeringMode", 2)
|
||||
|
||||
self._main_cruise_toggle.action_item.set_enabled(False)
|
||||
self._main_cruise_toggle.action_item.set_state(False)
|
||||
@@ -123,9 +122,9 @@ class MadsSettingsLayout(Widget):
|
||||
self._unified_engagement_toggle.action_item.set_state(True)
|
||||
self._unified_engagement_toggle.set_description("<b>" + DEFAULT_TO_ON + "</b><br>" + MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)
|
||||
|
||||
self._steering_mode.action_item.set_enabled(False)
|
||||
self._steering_mode.set_description(STATUS_DISENGAGE_ONLY)
|
||||
self._steering_mode.action_item.set_selected_button(MadsSteeringModeOnBrake.DISENGAGE)
|
||||
self._steering_mode.action_item.set_enabled_buttons({MadsSteeringModeOnBrake.DISENGAGE})
|
||||
self._steering_mode.action_item.set_selected_button(2)
|
||||
else:
|
||||
self._main_cruise_toggle.action_item.set_enabled(True)
|
||||
self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC)
|
||||
@@ -134,4 +133,3 @@ class MadsSettingsLayout(Widget):
|
||||
self._unified_engagement_toggle.set_description(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)
|
||||
|
||||
self._steering_mode.action_item.set_enabled(True)
|
||||
self._steering_mode.action_item.set_enabled_buttons(None)
|
||||
|
||||
@@ -355,10 +355,5 @@ class SunnylinkLayout(Widget):
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
ui_state.sunnylink_state.set_settings_open(True)
|
||||
self._scroller.show_event()
|
||||
self._sunnylink_description.set_visible(False)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
ui_state.sunnylink_state.set_settings_open(False)
|
||||
|
||||
@@ -6,14 +6,8 @@ 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.onroad.rainbow_path import RainbowPath
|
||||
|
||||
LANE_LINE_COLORS_SP = {
|
||||
UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255),
|
||||
UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255),
|
||||
}
|
||||
|
||||
|
||||
class ModelRendererSP:
|
||||
def __init__(self):
|
||||
self.rainbow_path = RainbowPath()
|
||||
|
||||
@@ -23,7 +23,7 @@ class AugmentedRoadViewSP:
|
||||
def update_fade_out_bottom_overlay(self, _content_rect):
|
||||
# Fade out bottom of overlays for looks (only when engaged)
|
||||
fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
|
||||
if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState' and fade_alpha > 1e-2:
|
||||
if ui_state.torque_bar and fade_alpha > 1e-2:
|
||||
# Scale the fade texture to the content rect
|
||||
rl.draw_texture_pro(self._fade_texture,
|
||||
rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height),
|
||||
|
||||
@@ -19,8 +19,8 @@ from openpilot.system.ui.widgets import Widget
|
||||
|
||||
class DeveloperUiRenderer(Widget):
|
||||
DEV_UI_OFF = 0
|
||||
DEV_UI_BOTTOM = 1
|
||||
DEV_UI_RIGHT = 2
|
||||
DEV_UI_RIGHT = 1
|
||||
DEV_UI_BOTTOM = 2
|
||||
DEV_UI_BOTH = 3
|
||||
BOTTOM_BAR_HEIGHT = 61
|
||||
|
||||
@@ -62,10 +62,10 @@ class DeveloperUiRenderer(Widget):
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
return
|
||||
|
||||
if self.dev_ui_mode == self.DEV_UI_BOTTOM:
|
||||
self._draw_bottom_dev_ui(rect)
|
||||
elif self.dev_ui_mode == self.DEV_UI_RIGHT:
|
||||
if self.dev_ui_mode == self.DEV_UI_RIGHT:
|
||||
self._draw_right_dev_ui(rect)
|
||||
elif self.dev_ui_mode == self.DEV_UI_BOTTOM:
|
||||
self._draw_bottom_dev_ui(rect)
|
||||
elif self.dev_ui_mode == self.DEV_UI_BOTH:
|
||||
self._draw_right_dev_ui(rect)
|
||||
self._draw_bottom_dev_ui(rect)
|
||||
|
||||
@@ -6,8 +6,9 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel
|
||||
@@ -16,11 +17,6 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartC
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.speed_renderer import SpeedRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer, UI_CONFIG, FONT_SIZES, COLORS, CRUISE_DISABLED_CHAR
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
|
||||
class HudRendererSP(HudRenderer):
|
||||
@@ -36,21 +32,7 @@ class HudRendererSP(HudRenderer):
|
||||
self.speed_renderer = SpeedRenderer()
|
||||
self._torque_bar = TorqueBar(scale=3.0, always=True)
|
||||
|
||||
self.pcm_cruise_speed: bool = True
|
||||
self.show_icbm_status: bool = False
|
||||
self.icbm_active_counter: int = 0
|
||||
self.speed_cluster: float = 0.0
|
||||
self.speed_conv: float = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
def _update_state(self) -> None:
|
||||
if ui_state.sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
return
|
||||
|
||||
if ui_state.CP_SP is not None:
|
||||
self.pcm_cruise_speed = ui_state.CP_SP.pcmCruiseSpeed
|
||||
self.speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
self.speed_cluster = ui_state.sm['carState'].cruiseState.speedCluster * self.speed_conv
|
||||
|
||||
super()._update_state()
|
||||
self.road_name_renderer.update()
|
||||
self.speed_limit_renderer.update()
|
||||
@@ -59,64 +41,6 @@ class HudRendererSP(HudRenderer):
|
||||
self.circular_alerts_renderer.update()
|
||||
self.speed_renderer.update()
|
||||
|
||||
def _get_icbm_status(self):
|
||||
if not self.pcm_cruise_speed and ui_state.sm['carControl'].enabled:
|
||||
if round(self.set_speed) != round(self.speed_cluster):
|
||||
self.icbm_active_counter = 3 * gui_app.target_fps # 3 seconds usually
|
||||
elif self.icbm_active_counter > 0:
|
||||
self.icbm_active_counter -= 1
|
||||
else:
|
||||
self.icbm_active_counter = 0
|
||||
|
||||
self.show_icbm_status = self.icbm_active_counter > 0
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
self._get_icbm_status()
|
||||
|
||||
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
|
||||
x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2
|
||||
y = rect.y + 45
|
||||
|
||||
set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height)
|
||||
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.BLACK_TRANSLUCENT)
|
||||
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.BORDER_TRANSLUCENT)
|
||||
|
||||
max_color = COLORS.GREY
|
||||
set_speed_color = COLORS.DARK_GREY
|
||||
if self.is_cruise_set:
|
||||
set_speed_color = COLORS.WHITE
|
||||
if ui_state.status == UIStatus.ENGAGED:
|
||||
max_color = COLORS.ENGAGED
|
||||
elif ui_state.status == UIStatus.DISENGAGED:
|
||||
max_color = COLORS.DISENGAGED
|
||||
elif ui_state.status == UIStatus.OVERRIDE:
|
||||
max_color = COLORS.OVERRIDE
|
||||
|
||||
max_str_size = 60 if self.show_icbm_status else 40
|
||||
max_str_y = 15 if self.show_icbm_status else 27
|
||||
|
||||
max_text = str(round(self.speed_cluster)) if self.show_icbm_status else tr("MAX")
|
||||
max_text_width = measure_text_cached(self._font_semi_bold, max_text, max_str_size).x
|
||||
rl.draw_text_ex(
|
||||
self._font_semi_bold,
|
||||
max_text,
|
||||
rl.Vector2(x + (set_speed_width - max_text_width) / 2, y + max_str_y),
|
||||
max_str_size,
|
||||
0,
|
||||
max_color,
|
||||
)
|
||||
|
||||
set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed))
|
||||
speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x
|
||||
rl.draw_text_ex(
|
||||
self._font_bold,
|
||||
set_speed_text,
|
||||
rl.Vector2(x + (set_speed_width - speed_text_width) / 2, y + 77),
|
||||
FONT_SIZES.set_speed,
|
||||
0,
|
||||
set_speed_color,
|
||||
)
|
||||
|
||||
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
|
||||
self.speed_renderer.render(rect)
|
||||
|
||||
|
||||
@@ -72,28 +72,72 @@ class TurnSignalWidget(Widget):
|
||||
|
||||
|
||||
class TurnSignalController:
|
||||
def __init__(self):
|
||||
self._config = TurnSignalConfig()
|
||||
def __init__(self, config: TurnSignalConfig | None = None):
|
||||
self._config = config or TurnSignalConfig()
|
||||
self._left_signal = TurnSignalWidget(direction=IconSide.left)
|
||||
self._right_signal = TurnSignalWidget(direction=IconSide.right)
|
||||
|
||||
@staticmethod
|
||||
def _update_signal(signal, blindspot, blinker):
|
||||
if ui_state.blindspot and blindspot:
|
||||
signal.activate('blind_spot')
|
||||
elif ui_state.turn_signals and blinker:
|
||||
signal.activate('signal')
|
||||
else:
|
||||
signal.deactivate()
|
||||
self._last_icon_side = None
|
||||
|
||||
def update(self):
|
||||
CS = ui_state.sm['carState']
|
||||
sm = ui_state.sm
|
||||
ss = sm['selfdriveState']
|
||||
|
||||
self._update_signal(self._left_signal, CS.leftBlindspot, CS.leftBlinker)
|
||||
self._update_signal(self._right_signal, CS.rightBlindspot, CS.rightBlinker)
|
||||
event_name = ss.alertType.split('/')[0] if ss.alertType else ''
|
||||
|
||||
if event_name == 'preLaneChangeLeft':
|
||||
self._last_icon_side = IconSide.left
|
||||
self._left_signal.activate('signal')
|
||||
self._right_signal.deactivate()
|
||||
|
||||
elif event_name == 'preLaneChangeRight':
|
||||
self._last_icon_side = IconSide.right
|
||||
self._right_signal.activate('signal')
|
||||
self._left_signal.deactivate()
|
||||
|
||||
elif event_name == 'laneChange':
|
||||
if self._last_icon_side == IconSide.left:
|
||||
self._left_signal.activate('signal')
|
||||
self._right_signal.deactivate()
|
||||
elif self._last_icon_side == IconSide.right:
|
||||
self._right_signal.activate('signal')
|
||||
self._left_signal.deactivate()
|
||||
|
||||
elif event_name == 'laneChangeBlocked':
|
||||
CS = sm['carState']
|
||||
if CS.leftBlinker:
|
||||
icon_side = IconSide.left
|
||||
elif CS.rightBlinker:
|
||||
icon_side = IconSide.right
|
||||
else:
|
||||
icon_side = self._last_icon_side
|
||||
|
||||
if icon_side == IconSide.left:
|
||||
self._left_signal.activate('blind_spot')
|
||||
self._right_signal.deactivate()
|
||||
elif icon_side == IconSide.right:
|
||||
self._right_signal.activate('blind_spot')
|
||||
self._left_signal.deactivate()
|
||||
|
||||
else:
|
||||
self._last_icon_side = None
|
||||
CS = sm['carState']
|
||||
|
||||
if CS.leftBlindspot:
|
||||
self._left_signal.activate('blind_spot')
|
||||
elif CS.leftBlinker:
|
||||
self._left_signal.activate('signal')
|
||||
else:
|
||||
self._left_signal.deactivate()
|
||||
|
||||
if CS.rightBlindspot:
|
||||
self._right_signal.activate('blind_spot')
|
||||
elif CS.rightBlinker:
|
||||
self._right_signal.activate('signal')
|
||||
else:
|
||||
self._right_signal.deactivate()
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
if not ui_state.turn_signals and not ui_state.blindspot:
|
||||
if not ui_state.turn_signals:
|
||||
return
|
||||
|
||||
x = rect.x + rect.width / 2
|
||||
|
||||
@@ -26,7 +26,6 @@ class OnroadTimerStatus(Enum):
|
||||
|
||||
class UIStateSP:
|
||||
def __init__(self):
|
||||
self.CP_SP: custom.CarParamsSP | None = None
|
||||
self.params = Params()
|
||||
self.sm_services_ext = [
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
@@ -39,9 +38,6 @@ class UIStateSP:
|
||||
self.onroad_brightness_timer: int = 0
|
||||
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
|
||||
self.reset_onroad_sleep_timer()
|
||||
self.CP_SP: custom.CarParamsSP | None = None
|
||||
self.has_icbm: bool = False
|
||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||
|
||||
def update(self) -> None:
|
||||
if self.sunnylink_enabled:
|
||||
@@ -124,7 +120,6 @@ class UIStateSP:
|
||||
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
|
||||
if CP_SP_bytes is not None:
|
||||
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
|
||||
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
|
||||
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
|
||||
self.blindspot = self.params.get_bool("BlindSpot")
|
||||
self.chevron_metrics = self.params.get("ChevronInfo")
|
||||
@@ -142,14 +137,15 @@ class UIStateSP:
|
||||
self.torque_bar = self.params.get_bool("TorqueBar")
|
||||
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
||||
self.turn_signals = self.params.get_bool("ShowTurnSignals")
|
||||
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
@staticmethod
|
||||
def _set_awake(on: bool, _ui_state):
|
||||
if _ui_state.boot_offroad_mode == 1 and not on:
|
||||
_ui_state.params.put_bool("OffroadMode", True)
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on and self._params.get("DeviceBootMode", return_default=True) == 1:
|
||||
self._params.put_bool("OffroadMode", True)
|
||||
|
||||
@staticmethod
|
||||
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import base64
|
||||
import webbrowser
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
@@ -24,6 +25,12 @@ def compare_frames(frame1_path, frame2_path):
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def frame_to_data_url(frame_path):
|
||||
with open(frame_path, 'rb') as f:
|
||||
data = f.read()
|
||||
return f"data:image/png;base64,{base64.b64encode(data).decode()}"
|
||||
|
||||
|
||||
def create_diff_video(video1, video2, output_path):
|
||||
"""Create a diff video using ffmpeg blend filter with difference mode."""
|
||||
print("Creating diff video...")
|
||||
@@ -53,16 +60,20 @@ def find_differences(video1, video2):
|
||||
|
||||
print(f"Comparing {len(frames1)} frames...")
|
||||
different_frames = []
|
||||
frame_data = []
|
||||
|
||||
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
|
||||
is_different = not compare_frames(f1, f2)
|
||||
if is_different:
|
||||
different_frames.append(i)
|
||||
|
||||
return different_frames, len(frames1)
|
||||
if i < 10 or i >= len(frames1) - 10 or is_different:
|
||||
frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)})
|
||||
|
||||
return different_frames, frame_data, len(frames1)
|
||||
|
||||
|
||||
def generate_html_report(video1, video2, basedir, different_frames, total_frames):
|
||||
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
|
||||
chunks = []
|
||||
if different_frames:
|
||||
current_chunk = [different_frames[0]]
|
||||
@@ -166,14 +177,14 @@ def main():
|
||||
diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4")
|
||||
create_diff_video(args.video1, args.video2, diff_video_path)
|
||||
|
||||
different_frames, total_frames = find_differences(args.video1, args.video2)
|
||||
different_frames, frame_data, total_frames = find_differences(args.video1, args.video2)
|
||||
|
||||
if different_frames is None:
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
print("Generating HTML report...")
|
||||
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames)
|
||||
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames)
|
||||
|
||||
with open(DIFF_OUT_DIR / args.output, 'w') as f:
|
||||
f.write(html)
|
||||
|
||||
1218
selfdrive/ui/translations/app_ar.po
Normal file
1218
selfdrive/ui/translations/app_ar.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
||||
"Español": "es",
|
||||
"Türkçe": "tr",
|
||||
"Українська": "uk",
|
||||
"العربية": "ar",
|
||||
"ไทย": "th",
|
||||
"中文(繁體)": "zh-CHT",
|
||||
"中文(简体)": "zh-CHS",
|
||||
|
||||
@@ -299,7 +299,7 @@ class Device(DeviceSP):
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
DeviceSP._set_awake(on, ui_state)
|
||||
DeviceSP._set_awake(self, on)
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
|
||||
@@ -2,3 +2,4 @@ SConscript(['common/transformations/SConscript'])
|
||||
SConscript(['modeld/SConscript'])
|
||||
SConscript(['modeld_v2/SConscript'])
|
||||
SConscript(['selfdrive/locationd/SConscript'])
|
||||
SConscript(['modeld_sunny/SConscript'])
|
||||
|
||||
32
sunnypilot/modeld_sunny/SConscript
Normal file
32
sunnypilot/modeld_sunny/SConscript
Normal file
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
Import('env', 'arch')
|
||||
lenv = env.Clone()
|
||||
tinygrad_repo = env.Dir("#tinygrad_repo")
|
||||
tinygrad_files = ["#"+x for x in glob.glob(tinygrad_repo.relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]
|
||||
|
||||
def mayo_compile(flags, model_name):
|
||||
pythonpath_string = f'PYTHONPATH="${{PYTHONPATH}}:{tinygrad_repo.abspath}"'
|
||||
onnx_fn = f"distilled_models/{model_name}.onnx"
|
||||
pkl_fn = f"distilled_models/{model_name}_tinygrad.pkl"
|
||||
if not os.path.exists("distilled_models"):
|
||||
try:
|
||||
os.makedirs("distilled_models")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if os.path.isfile(File(onnx_fn).abspath):
|
||||
return lenv.Command(
|
||||
pkl_fn,
|
||||
[onnx_fn, "compile_split_tinygrad.py"] + tinygrad_files,
|
||||
f'{pythonpath_string} {flags} python3 {File("compile_split_tinygrad.py").abspath} {File(onnx_fn).abspath} {File(pkl_fn).abspath}'
|
||||
)
|
||||
|
||||
flags = {
|
||||
'larch64': 'DEV=QCOM',
|
||||
'Darwin': f'DEV=METAL HOME={os.path.expanduser("~")} IMAGE=0',
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
|
||||
|
||||
for m in ["student_vision", "student_policy"]:
|
||||
mayo_compile(flags, m)
|
||||
0
sunnypilot/modeld_sunny/__init__.py
Normal file
0
sunnypilot/modeld_sunny/__init__.py
Normal file
109
sunnypilot/modeld_sunny/compile_split_tinygrad.py
Normal file
109
sunnypilot/modeld_sunny/compile_split_tinygrad.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
The whole point of this module is to mimic compile3.py while adapting it for our buffers to prevent buffer explosion
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import pickle
|
||||
|
||||
from tinygrad import Tensor, TinyJit, Context, Device
|
||||
from tinygrad.device import Buffer
|
||||
# from tinygrad.nn.onnx import OnnxRunner
|
||||
from tinygrad.frontend.onnx import OnnxRunner
|
||||
|
||||
if "JIT_BATCH_SIZE" not in os.environ:
|
||||
os.environ["JIT_BATCH_SIZE"] = "0"
|
||||
if "FLOAT16" not in os.environ:
|
||||
os.environ["FLOAT16"] = "1"
|
||||
if "OPT" not in os.environ:
|
||||
os.environ["OPT"] = "99"
|
||||
|
||||
|
||||
KEEP_BUFFERS = set()
|
||||
original_reduce = Buffer.__reduce__
|
||||
def stripped_reduce(self):
|
||||
if id(self) in KEEP_BUFFERS:
|
||||
return original_reduce(self)
|
||||
return (self.__class__, (self.device, self.size, self.dtype))
|
||||
Buffer.__reduce__ = stripped_reduce
|
||||
|
||||
|
||||
def compile_model(onnx_path, output_path, input_shapes=None, input_types=None):
|
||||
print(f"Compiling {onnx_path} -> {output_path}")
|
||||
run_onnx = OnnxRunner(onnx_path)
|
||||
|
||||
if input_shapes is None:
|
||||
input_shapes = {name: spec.shape for name, spec in run_onnx.graph_inputs.items()}
|
||||
if input_types is None:
|
||||
input_types = {name: spec.dtype for name, spec in run_onnx.graph_inputs.items()}
|
||||
|
||||
Tensor.manual_seed(100)
|
||||
inputs = {k: Tensor(Tensor.randn(*shp, dtype=input_types[k]).mul(8).realize().numpy(), device='NPY') for k, shp in sorted(input_shapes.items())}
|
||||
inputs = {k: v.to(Device.DEFAULT).realize() for k, v in inputs.items()}
|
||||
print(f"Realized all {len(inputs)} inputs on {Device.DEFAULT}")
|
||||
|
||||
input_buf_ids = set()
|
||||
for _, v in inputs.items():
|
||||
if hasattr(v, '_buffer'):
|
||||
try:
|
||||
b = v._buffer()
|
||||
if b is not None:
|
||||
input_buf_ids.add(id(b))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if "vision" in onnx_path:
|
||||
onnx_jit = TinyJit(lambda **kwargs: next(iter(run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values())).cast('float32'), prune=True)
|
||||
else:
|
||||
onnx_jit = TinyJit(lambda **kwargs: [x.cast('float32').contiguous().realize() for x in run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values()], prune=True)
|
||||
|
||||
for i in range(3):
|
||||
with Context(DEBUG=max(int(os.getenv("DEBUG", 0)), 2 if i == 2 else 1)):
|
||||
res = onnx_jit(**inputs)
|
||||
if isinstance(res, list):
|
||||
for x in res:
|
||||
x.numpy()
|
||||
else:
|
||||
res.numpy()
|
||||
print(f"Captured {len(onnx_jit.captured.jit_cache)} kernels")
|
||||
|
||||
all_read_ids = set()
|
||||
all_written_ids = set()
|
||||
|
||||
for ji in onnx_jit.captured.jit_cache:
|
||||
if len(ji.bufs) > 0:
|
||||
if ji.bufs[0] is not None:
|
||||
all_written_ids.add(id(ji.bufs[0]))
|
||||
|
||||
for b in ji.bufs[1:]:
|
||||
if b is not None:
|
||||
all_read_ids.add(id(b))
|
||||
|
||||
weight_candidates = all_read_ids - all_written_ids
|
||||
weight_ids = weight_candidates - input_buf_ids
|
||||
print(f"Identified {len(weight_ids)} weight candidates (Read-Only & Not Input).")
|
||||
total_weight_size = 0
|
||||
|
||||
marked_count = 0
|
||||
for ji in onnx_jit.captured.jit_cache:
|
||||
for b in ji.bufs:
|
||||
if b is not None and id(b) in weight_ids:
|
||||
if id(b) not in KEEP_BUFFERS:
|
||||
KEEP_BUFFERS.add(id(b))
|
||||
total_weight_size += b.size * b.dtype.itemsize
|
||||
marked_count += 1
|
||||
|
||||
print(f"Preserving {marked_count} unique weight buffers.")
|
||||
print(f"Total Preserved Weight Data Size: {total_weight_size / 1e6:.2f} MB")
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
pickle.dump(onnx_jit, f)
|
||||
print(f"Saved {output_path}, pkl size: {os.path.getsize(output_path)/1e6:.2f} MB")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python compile_split_tinygrad.py <input_onnx> <output_pkl>")
|
||||
sys.exit(1)
|
||||
|
||||
input_onnx = sys.argv[1]
|
||||
output_pkl = sys.argv[2]
|
||||
compile_model(input_onnx, output_pkl)
|
||||
161
sunnypilot/modeld_sunny/fill_model_msg.py
Normal file
161
sunnypilot/modeld_sunny/fill_model_msg.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, get_curvature_from_plan
|
||||
|
||||
|
||||
def interp_vec(t_out, t_in, vec):
|
||||
# vec shape (N, 3), output (M, 3)
|
||||
return np.stack([np.interp(t_out, t_in, vec[:, i]) for i in range(3)], axis=1)
|
||||
|
||||
|
||||
def fill_alpamayo_msg(modelV2, net_outputs, frame_id, frame_drop_ratio, timestamp_eof, CP, lat_delay, v_ego):
|
||||
modelV2.frameId = frame_id
|
||||
modelV2.frameIdExtra = frame_id
|
||||
modelV2.timestampEof = timestamp_eof
|
||||
modelV2.frameDropPerc = frame_drop_ratio * 100.0
|
||||
|
||||
modelV2.init('laneLines', 4)
|
||||
modelV2.init('roadEdges', 2)
|
||||
modelV2.init('laneLineProbs', 4)
|
||||
modelV2.init('roadEdgeStds', 2)
|
||||
|
||||
for i in range(4):
|
||||
l = modelV2.laneLines[i]
|
||||
l.t = [0.0]
|
||||
l.x = [0.0]
|
||||
l.y = [0.0]
|
||||
l.z = [0.0]
|
||||
modelV2.laneLineProbs[i] = 0.0
|
||||
|
||||
for i in range(2):
|
||||
e = modelV2.roadEdges[i]
|
||||
e.t = [0.0]
|
||||
e.x = [0.0]
|
||||
e.y = [0.0]
|
||||
e.z = [0.0]
|
||||
modelV2.roadEdgeStds[i] = 1.0
|
||||
|
||||
|
||||
leads = modelV2.init('leadsV3', 1)
|
||||
lead = leads[0]
|
||||
pred_lead = net_outputs['pred_lead'][0]
|
||||
prob_logit = float(pred_lead[0])
|
||||
dist_pred = float(pred_lead[1] * 100.0)
|
||||
dist_sigma = float(np.exp(pred_lead[2]))
|
||||
|
||||
v_rel_pred = float(pred_lead[3])
|
||||
v_sigma = float(np.exp(pred_lead[4]))
|
||||
|
||||
a_rel_pred = float(pred_lead[5])
|
||||
a_sigma = float(np.exp(pred_lead[6]))
|
||||
|
||||
prob = float(1.0 / (1.0 + np.exp(-prob_logit)))
|
||||
|
||||
lead.prob = prob
|
||||
lead.probTime = 0.0
|
||||
|
||||
# X(t) = X0 + V_rel*t + 0.5*A_rel*t^2
|
||||
T = ModelConstants.T_IDXS
|
||||
lead.t = list(T)
|
||||
lead.x = [float(dist_pred + v_rel_pred * t + 0.5 * a_rel_pred * t**2) for t in T]
|
||||
lead.v = [float(v_ego + v_rel_pred + a_rel_pred * t) for t in T]
|
||||
a_ego = net_outputs['acceleration'][0, 0, 0] # T=0 ego accel estimate (x component)
|
||||
lead.a = [float(a_ego + a_rel_pred)] * len(T)
|
||||
lead.y = [0.0] * len(T)
|
||||
|
||||
lead.xStd = [max(0.5, dist_sigma * 100.0)] * len(T)
|
||||
lead.yStd = [1.0] * len(T)
|
||||
lead.vStd = [max(0.1, v_sigma)] * len(T)
|
||||
lead.aStd = [max(0.1, a_sigma)] * len(T)
|
||||
|
||||
modelV2.meta.engagedProb = 1.0
|
||||
desire_pred = [0.0] * 8
|
||||
|
||||
if 'pred_light' in net_outputs:
|
||||
red_prob = float(1.0 / (1.0 + np.exp(-net_outputs['pred_light'][0, 1] + net_outputs['pred_light'][0, 0])))
|
||||
desire_pred[4] = red_prob
|
||||
|
||||
modelV2.meta.desirePrediction = desire_pred
|
||||
modelV2.meta.desireState = [0.0] * 8
|
||||
reasoning_error = net_outputs.get('consistency_error', 0.0)
|
||||
|
||||
if reasoning_error < 0.5:
|
||||
modelV2.confidence = "green"
|
||||
elif reasoning_error < 1.5:
|
||||
modelV2.confidence = "yellow"
|
||||
else:
|
||||
modelV2.confidence = "red"
|
||||
|
||||
ALPAMAYO_T_IDXS = np.arange(1, 65) * 0.1 # 64 steps at .1s intervals
|
||||
t_idxs = ModelConstants.T_IDXS
|
||||
t_all = np.concatenate(([0.0], ALPAMAYO_T_IDXS)) # this model starts at t=0.1 so if we prepend 0.0 and interpolate for t=now it should match op
|
||||
|
||||
pos_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs['position'][0])))
|
||||
pos_std_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs.get('position_std', np.ones((64, 3)) * 0.1))))
|
||||
vel_interp = interp_vec(t_idxs, t_all, np.vstack(([v_ego, 0.0, 0.0], net_outputs['velocity'][0])))
|
||||
acc_interp = interp_vec(t_idxs, t_all, np.vstack((net_outputs['acceleration'][0][0], net_outputs['acceleration'][0])))
|
||||
rot_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs['orientation'][0])))
|
||||
rate_interp = interp_vec(t_idxs, t_all, np.vstack((net_outputs['orientation_rate'][0][0], net_outputs['orientation_rate'][0])))
|
||||
|
||||
# https://www.mathworks.com/help/vdynblks/ug/coordinate-systems-in-vehicle-dynamics-blockset.html
|
||||
# following SAE J670 and ISO 8855, for sunnymayo model: x is forward (f), y is left (lat), z is up/vert
|
||||
# Openpilot Modelv2 and camerad expects SAE J670: x is forward, y is right, z is down
|
||||
|
||||
modelV2.position.t = t_idxs # time, obviously
|
||||
modelV2.position.x = pos_interp[:, 0].tolist() # f dist
|
||||
modelV2.position.y = (-pos_interp[:, 1]).tolist() # lat offset (Flip L->R)
|
||||
modelV2.position.z = (-pos_interp[:, 2]).tolist() # vert offset (Flip U->D) (elevation)
|
||||
modelV2.position.xStd = pos_std_interp[:, 0].tolist()
|
||||
modelV2.position.yStd = pos_std_interp[:, 1].tolist()
|
||||
modelV2.position.zStd = pos_std_interp[:, 2].tolist()
|
||||
|
||||
modelV2.velocity.t = t_idxs
|
||||
modelV2.velocity.x = vel_interp[:, 0].tolist() # f vel (vego)
|
||||
modelV2.velocity.y = (-vel_interp[:, 1]).tolist() # lat vel (curvature)
|
||||
modelV2.velocity.z = (-vel_interp[:, 2]).tolist() # vert vel
|
||||
|
||||
modelV2.acceleration.t = t_idxs
|
||||
modelV2.acceleration.x = acc_interp[:, 0].tolist() # f accel (aego)
|
||||
modelV2.acceleration.y = (-acc_interp[:, 1]).tolist() # lat accel
|
||||
modelV2.acceleration.z = (-acc_interp[:, 2]).tolist() # vert accel
|
||||
|
||||
modelV2.orientation.t = t_idxs
|
||||
modelV2.orientation.x = rot_interp[:, 0].tolist() # roll (treated as 0)
|
||||
modelV2.orientation.y = (-rot_interp[:, 1]).tolist() # pitch (from z-slope)
|
||||
modelV2.orientation.z = (-rot_interp[:, 2]).tolist() # yaw (heading)
|
||||
|
||||
modelV2.orientationRate.t = t_idxs
|
||||
modelV2.orientationRate.x = rate_interp[:, 0].tolist() # roll rate
|
||||
modelV2.orientationRate.y = (-rate_interp[:, 1]).tolist() # pitch rate (Flip U->D)
|
||||
modelV2.orientationRate.z = (-rate_interp[:, 2]).tolist() # yaw rate (Flip L->R)
|
||||
|
||||
long_action_t = CP.longitudinalActuatorDelay + DT_MDL
|
||||
desired_accel, should_stop = get_accel_from_plan(vel_interp[:, 0], acc_interp[:, 0], t_idxs, action_t=long_action_t)
|
||||
modelV2.action.desiredAcceleration = float(desired_accel)
|
||||
modelV2.action.shouldStop = bool(should_stop)
|
||||
|
||||
lat_action_t = lat_delay + DT_MDL
|
||||
desired_curvature = get_curvature_from_plan(-rot_interp[:, 2], -rate_interp[:, 2], t_idxs, vego=v_ego, action_t=lat_action_t)
|
||||
modelV2.action.desiredCurvature = float(desired_curvature)
|
||||
|
||||
|
||||
def fill_pose_msg(camera_odometry, net_outputs, frame_id, timestamp_eof):
|
||||
camera_odometry.frameId = frame_id
|
||||
camera_odometry.timestampEof = timestamp_eof
|
||||
|
||||
trans = net_outputs['velocity'][0, 0].copy()
|
||||
trans[1] *= -1.0
|
||||
trans[2] *= -1.0
|
||||
camera_odometry.trans = trans.tolist()
|
||||
|
||||
std_val = float(max(0.01, net_outputs.get('consistency_error', 0.1)))
|
||||
camera_odometry.transStd = [std_val, std_val, std_val]
|
||||
rot = net_outputs['orientation_rate'][0, 0].copy()
|
||||
rot[1] *= -1.0
|
||||
rot[2] *= -1.0
|
||||
camera_odometry.rot = rot.tolist()
|
||||
|
||||
rot_std = std_val * 0.1
|
||||
camera_odometry.rotStd = [rot_std, rot_std, rot_std]
|
||||
51
sunnypilot/modeld_sunny/input_id_helper.py
Normal file
51
sunnypilot/modeld_sunny/input_id_helper.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlpamayoDesire:
|
||||
DRIVE_SAFELY = 0
|
||||
TURN_LEFT = 2
|
||||
TURN_RIGHT = 1
|
||||
DRIVE_FAST = 3
|
||||
STOP = 4
|
||||
|
||||
|
||||
class InputIDHelper:
|
||||
def __init__(self):
|
||||
self.current_ids = np.zeros((1, 16), dtype=np.int64)
|
||||
self.desire = AlpamayoDesire.DRIVE_SAFELY
|
||||
self.params = Params()
|
||||
self.drive_fast = False
|
||||
self.msg_count = -1
|
||||
|
||||
def update_params(self):
|
||||
if self.msg_count % 60 == 0:
|
||||
self.drive_fast = self.params.get_bool("AlpamayoDriveFast")
|
||||
self.msg_count += 1
|
||||
|
||||
def update(self, sm):
|
||||
self.update_params()
|
||||
if sm is None:
|
||||
return self.current_ids
|
||||
|
||||
left_blinker = False
|
||||
right_blinker = False
|
||||
|
||||
if sm.seen['carState']:
|
||||
left_blinker = sm['carState'].leftBlinker
|
||||
right_blinker = sm['carState'].rightBlinker
|
||||
|
||||
# Priority: STOP (TODO) > Turn > Drive Fast > Drive Safely
|
||||
new_desire = AlpamayoDesire.DRIVE_SAFELY
|
||||
if left_blinker:
|
||||
new_desire = AlpamayoDesire.TURN_LEFT
|
||||
elif right_blinker:
|
||||
new_desire = AlpamayoDesire.TURN_RIGHT
|
||||
elif self.drive_fast:
|
||||
new_desire = AlpamayoDesire.DRIVE_FAST
|
||||
|
||||
self.desire = new_desire
|
||||
self.current_ids.fill(self.desire)
|
||||
return self.current_ids
|
||||
60
sunnypilot/modeld_sunny/kinematic_model.py
Normal file
60
sunnypilot/modeld_sunny/kinematic_model.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
|
||||
def action_to_traj(action: Tensor, v0: Tensor, dt: float = 0.1):
|
||||
"""
|
||||
This function is a lightweight tinygrad transformation of the unicycle accel physics model based on Nvidia's
|
||||
unicycle model https://github.com/NVlabs/alpamayo/blob/main/src/alpamayo_r1/action_space/unicycle_accel_curvature.py
|
||||
|
||||
Integrate action (accel, kappa) to trajectory (x, y, theta)
|
||||
Args:
|
||||
action: (B, T, 2) [accel, kappa]
|
||||
v0: (B,) Initial velocity
|
||||
dt: Time step
|
||||
Returns:
|
||||
res: Dict containing position, velocity, acceleration, orientation, orientation_rate
|
||||
"""
|
||||
B, T, _ = action.shape
|
||||
ACCEL_MEAN = 0.02902695
|
||||
ACCEL_STD = 0.68104267
|
||||
CURV_MEAN = 0.00026922
|
||||
CURV_STD = 0.02614828
|
||||
|
||||
accel = action[..., 0] * ACCEL_STD + ACCEL_MEAN
|
||||
kappa = action[..., 1] * CURV_STD + CURV_MEAN
|
||||
|
||||
# v_{t+1} = v_t + a_t * dt
|
||||
v_diff = accel * dt
|
||||
v_seq = v_diff.cumsum(axis=1) + v0.reshape(B, 1) # cumulative sum over T dimension (axis 1)
|
||||
velocity = v0.reshape(B, 1).cat(v_seq, dim=1)
|
||||
|
||||
# theta_{t+1} = theta_t + kappa_t * (v_t * dt + 0.5 * a_t * dt^2)
|
||||
dt_2_term = 0.5 * (dt**2)
|
||||
dtheta = kappa * (velocity[:, :-1] * dt + accel * dt_2_term)
|
||||
theta_seq = dtheta.cumsum(axis=1)
|
||||
theta = Tensor.zeros(B, 1, device=action.device, dtype=action.dtype).cat(theta_seq, dim=1)
|
||||
|
||||
# trapezoidal euler
|
||||
half_dt = 0.5 * dt
|
||||
v_cos = velocity * theta.cos()
|
||||
v_sin = velocity * theta.sin()
|
||||
|
||||
dx = (v_cos[:, :-1] + v_cos[:, 1:]) * half_dt
|
||||
dy = (v_sin[:, :-1] + v_sin[:, 1:]) * half_dt
|
||||
x = dx.cumsum(axis=1)
|
||||
y = dy.cumsum(axis=1)
|
||||
|
||||
res = {}
|
||||
res['action'] = accel.stack(kappa, dim=-1) # raw model output
|
||||
|
||||
# (x, y, 0)
|
||||
res['position'] = x.stack(y, Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
|
||||
# (vx, vy, 0)
|
||||
res['velocity'] = v_cos[:, 1:].stack(v_sin[:, 1:], Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
|
||||
# ax = accel * cos(theta), ay = accel * sin(theta), 0
|
||||
res['acceleration'] = (accel * theta[:, 1:].cos()).stack(accel * theta[:, 1:].sin(), Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
|
||||
# (0, 0, theta)
|
||||
res['orientation'] = Tensor.zeros(B, T, device=action.device, dtype=action.dtype).stack(Tensor.zeros(B, T, device=action.device, dtype=action.dtype), theta[:, 1:], dim=-1)
|
||||
# (0, 0, dtheta/dt)
|
||||
res['orientation_rate'] = Tensor.zeros(B, T, device=action.device, dtype=action.dtype).stack(Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dtheta / dt, dim=-1)
|
||||
return res
|
||||
19
sunnypilot/modeld_sunny/loader.py
Normal file
19
sunnypilot/modeld_sunny/loader.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
|
||||
def load_compiled_model(model_name: str = "student"):
|
||||
pkl_path = Path(__file__).parent / "distilled_models" / f"{model_name}_tinygrad.pkl"
|
||||
|
||||
if not pkl_path.exists():
|
||||
cloudlog.error(f"Compiled model not found at {pkl_path}")
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(pkl_path, "rb") as f:
|
||||
model_run = pickle.load(f)
|
||||
return model_run
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to load compiled Tinygrad model: {e}")
|
||||
return None
|
||||
344
sunnypilot/modeld_sunny/modeld.py
Normal file
344
sunnypilot/modeld_sunny/modeld.py
Normal file
@@ -0,0 +1,344 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import os
|
||||
import platform
|
||||
from setproctitle import setproctitle
|
||||
from openpilot.system.hardware import TICI
|
||||
|
||||
if TICI:
|
||||
os.environ['DEV'] = 'QCOM'
|
||||
elif platform.system() == "Darwin":
|
||||
os.environ['DEV'] = "METAL"
|
||||
else:
|
||||
os.environ['DEV'] = 'CPU'
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['DEV'] = 'AMD'
|
||||
os.environ['AMD_IFACE'] = 'USB'
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.dtype import dtypes
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal import car
|
||||
from cereal.messaging import PubMaster, SubMaster
|
||||
from msgq.visionipc import VisionIpcClient, VisionStreamType
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process, DT_MDL
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.transformations.model import bigmodel_frame_from_calib_frame
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS, view_frame_from_device_frame
|
||||
from openpilot.common.transformations.orientation import rot_from_euler
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext
|
||||
from openpilot.sunnypilot.modeld_sunny.kinematic_model import action_to_traj
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
from openpilot.sunnypilot.modeld_sunny.loader import load_compiled_model
|
||||
from openpilot.sunnypilot.modeld_sunny.input_id_helper import InputIDHelper
|
||||
from openpilot.sunnypilot.modeld_sunny.fill_model_msg import fill_alpamayo_msg, fill_pose_msg
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.openpilot.sunnypilot.modeld_sunny"
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
frame_id: int = 0
|
||||
timestamp_sof: int = 0
|
||||
timestamp_eof: int = 0
|
||||
|
||||
def __init__(self, vipc=None):
|
||||
if vipc is not None:
|
||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||
|
||||
|
||||
def safe_exp(x):
|
||||
return np.exp(np.clip(x, -np.inf, 11))
|
||||
|
||||
|
||||
def softmax(x, axis=-1):
|
||||
x = x - np.max(x, axis=axis, keepdims=True)
|
||||
x = safe_exp(x)
|
||||
return x / np.sum(x, axis=axis, keepdims=True)
|
||||
|
||||
|
||||
class AlpamayoModelD:
|
||||
def __init__(self, context: CLContext):
|
||||
self.params = Params()
|
||||
self.context = context
|
||||
|
||||
self.model_vision = load_compiled_model("student_vision")
|
||||
self.model_policy = load_compiled_model("student_policy")
|
||||
self.model_loaded = self.model_vision is not None and self.model_policy is not None
|
||||
|
||||
self.vision_input_names = ['road', 'wide']
|
||||
self.vision_input_shapes = {
|
||||
'road': (1, 3, 512, 1024),
|
||||
'wide': (1, 3, 512, 1024)
|
||||
}
|
||||
|
||||
self.frames = {name: DrivingModelFrame(context, 1024, 512, buffer_length=4) for name in self.vision_input_names}
|
||||
self.history_buffer = np.zeros((16, 3), dtype=np.float32)
|
||||
self.logic_pulse = np.zeros((1, 2048), dtype=np.float32)
|
||||
|
||||
def run(self, bufs, transforms, inputs, prepare_only):
|
||||
if prepare_only:
|
||||
return None
|
||||
if not hasattr(self, 'vision_inputs'):
|
||||
self.vision_inputs = {}
|
||||
|
||||
imgs_cl = {n: self.frames[n].prepare(bufs[n], transforms[n].flatten()) for n in self.vision_input_names if bufs.get(n)}
|
||||
|
||||
if TICI and not USBGPU:
|
||||
for k, v in imgs_cl.items():
|
||||
if k not in self.vision_inputs:
|
||||
self.vision_inputs[k] = qcom_tensor_from_opencl_address(v.mem_address, self.vision_input_shapes[k], dtype=dtypes.uint8)
|
||||
else:
|
||||
for k, v in imgs_cl.items():
|
||||
self.vision_inputs[k] = Tensor(self.frames[k].buffer_from_cl(v).reshape(self.vision_input_shapes[k]), dtype=dtypes.uint8).realize()
|
||||
|
||||
img_t = Tensor.stack([self.vision_inputs['wide'].cast(dtypes.float32) / 255.0,
|
||||
self.vision_inputs['road'].cast(dtypes.float32) / 255.0], dim=1).unsqueeze(0)
|
||||
|
||||
vis_res = self.model_vision(
|
||||
history=Tensor(inputs["history"]).contiguous().realize(),
|
||||
img=img_t.contiguous().realize(),
|
||||
input_ids=Tensor(inputs["input_ids"]).contiguous().realize(),
|
||||
logic_pulse=Tensor(inputs["logic_pulse"]).contiguous().realize()
|
||||
)
|
||||
context = vis_res.contiguous().realize()
|
||||
|
||||
x_input = Tensor.zeros(1, 64, 2, device=os.environ.get("DEV"), dtype=dtypes.float32)
|
||||
v_mu, v_std, pred_pulse, state_mu, state_std, pred_light, pred_lead, hypot_logits = self.model_policy(
|
||||
context=context,
|
||||
noisy_action=x_input.contiguous().realize(),
|
||||
t=Tensor(np.array([[0.0]], dtype=np.float32)).contiguous().realize(), # t=0
|
||||
traffic=Tensor(inputs["traffic_convention"]).contiguous().realize()
|
||||
)
|
||||
|
||||
weights = softmax(hypot_logits.numpy(), axis=1) # (B, M)
|
||||
winner_idx = np.argmax(weights[0])
|
||||
|
||||
v_winner = v_mu[:, winner_idx]
|
||||
state_winner = state_mu[:, winner_idx]
|
||||
state_std_winner = state_std[:, winner_idx]
|
||||
|
||||
outputs_tg = action_to_traj(v_winner, Tensor([inputs["v_ego"]], dtype=dtypes.float32), dt=0.1)
|
||||
outputs = {k: v.numpy() for k, v in outputs_tg.items()}
|
||||
outputs.update({
|
||||
"pred_pulse": pred_pulse.numpy(),
|
||||
"pred_light": pred_light[0:1].numpy(),
|
||||
"pred_lead": pred_lead[0:1].numpy(),
|
||||
"weights": weights[0]
|
||||
})
|
||||
|
||||
# Inject world positions for Z/Pitch
|
||||
pos_world = state_winner[0].numpy()
|
||||
pos_std = np.exp(state_std_winner[0].numpy())
|
||||
outputs["position"][0, :, 2] = pos_world[:, 2]
|
||||
outputs["position_std"] = pos_std # log_sigma -> sigma
|
||||
d_pos = np.diff(outputs["position"][0], axis=0, prepend=np.zeros((1, 3)))
|
||||
d_dist = np.maximum(np.linalg.norm(d_pos[:, :2], axis=1), 1e-4)
|
||||
pitch = np.arctan2(np.diff(pos_world[:, 2], prepend=0.0), d_dist)
|
||||
outputs["orientation"][0, :, 1] = pitch
|
||||
outputs["orientation_rate"][0, :, 1] = np.diff(pitch, prepend=0.0) / 0.1
|
||||
outputs["velocity"][0, :, 2] = np.linalg.norm(outputs["velocity"][0, :, :2], axis=1) * np.tan(pitch)
|
||||
outputs["consistency_error"] = float(np.mean(np.linalg.norm(outputs["position"][0] - pos_world, axis=1)))
|
||||
|
||||
return outputs
|
||||
|
||||
|
||||
def main():
|
||||
setproctitle(PROCESS_NAME)
|
||||
config_realtime_process(7, 54)
|
||||
# Loop runs at 20Hz to match camera acquisition.
|
||||
# Model inference runs at 10Hz via frame skipping.
|
||||
rk = Ratekeeper(1.0 / DT_MDL)
|
||||
cl_context = CLContext()
|
||||
modeld = AlpamayoModelD(cl_context)
|
||||
|
||||
# Load CarParams
|
||||
cloudlog.warning("Modeld: Waiting for CarParams...")
|
||||
CP = messaging.log_from_bytes(Params().get("CarParams", block=True), car.CarParams)
|
||||
cloudlog.warning("Modeld: Got CarParams")
|
||||
|
||||
camera_offset_helper = CameraOffsetHelper()
|
||||
input_id_helper = InputIDHelper()
|
||||
|
||||
if modeld.model_loaded:
|
||||
cloudlog.warning("Modeld: Successfully loaded compiled student model.")
|
||||
|
||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
|
||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "liveDelay", "livePose", "driverMonitoringState"])
|
||||
|
||||
# VisionIPC Clients
|
||||
while True:
|
||||
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
||||
if available_streams:
|
||||
use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams
|
||||
main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams
|
||||
break
|
||||
time.sleep(.1)
|
||||
|
||||
vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD
|
||||
vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context)
|
||||
vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context)
|
||||
cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}")
|
||||
|
||||
while not vipc_client_main.connect(False):
|
||||
time.sleep(0.1)
|
||||
while use_extra_client and not vipc_client_extra.connect(False):
|
||||
time.sleep(0.1)
|
||||
|
||||
cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})")
|
||||
if use_extra_client:
|
||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||
|
||||
model_transform_main = np.zeros((3, 3), dtype=np.float32)
|
||||
model_transform_extra = np.zeros((3, 3), dtype=np.float32)
|
||||
buf_main, buf_extra = None, None
|
||||
meta_main = FrameMeta()
|
||||
meta_extra = FrameMeta()
|
||||
|
||||
# filter to track dropped frames
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / 20.0)
|
||||
last_vipc_frame_id = 0
|
||||
run_count = 0
|
||||
lat_delay = 0.0
|
||||
live_calib_seen = False
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
|
||||
buf_main = vipc_client_main.recv()
|
||||
meta_main = FrameMeta(vipc_client_main)
|
||||
if buf_main is None:
|
||||
break
|
||||
|
||||
if buf_main is None:
|
||||
cloudlog.debug("vipc_client_main no frame")
|
||||
continue
|
||||
|
||||
if use_extra_client:
|
||||
# Keep receiving extra frames until frame id matches main camera
|
||||
while True:
|
||||
buf_extra = vipc_client_extra.recv()
|
||||
meta_extra = FrameMeta(vipc_client_extra)
|
||||
if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
|
||||
break
|
||||
|
||||
if buf_extra is None:
|
||||
cloudlog.debug("vipc_client_extra no frame")
|
||||
continue
|
||||
|
||||
if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000:
|
||||
cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\
|
||||
extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})")
|
||||
|
||||
else:
|
||||
# Use single camera
|
||||
buf_extra = buf_main
|
||||
meta_extra = meta_main
|
||||
|
||||
# 10Hz Execution Check (Skip odd frames)
|
||||
# We use main camera frameId as the clock
|
||||
if meta_main.frame_id % 2 != 0:
|
||||
last_vipc_frame_id = meta_main.frame_id
|
||||
continue
|
||||
|
||||
sm.update(0)
|
||||
v_ego = sm['carState'].vEgo if sm.seen['carState'] else 0.0
|
||||
|
||||
yaw_rate = 0.0
|
||||
if sm.seen['livePose'] and sm['livePose'].angularVelocityDevice.valid:
|
||||
yaw_rate = sm['livePose'].angularVelocityDevice.z
|
||||
|
||||
if sm.frame % 60 == 0:
|
||||
lat_delay = get_lat_delay(modeld.params, sm["liveDelay"].lateralDelay)
|
||||
camera_offset_helper.set_offset(modeld.params.get("CameraOffset", return_default=True))
|
||||
|
||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
calib_from_bigmodel = np.linalg.inv(bigmodel_frame_from_calib_frame[:, :3])
|
||||
device_from_calib = rot_from_euler(device_from_calib_euler)
|
||||
camera_from_calib_main = (dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics) @ view_frame_from_device_frame @ device_from_calib
|
||||
model_transform_main = camera_from_calib_main @ calib_from_bigmodel
|
||||
camera_from_calib_extra = dc.ecam.intrinsics @ view_frame_from_device_frame @ device_from_calib
|
||||
model_transform_extra = camera_from_calib_extra @ calib_from_bigmodel
|
||||
|
||||
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
|
||||
live_calib_seen = True
|
||||
|
||||
# Track dropped frames
|
||||
vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1)
|
||||
frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10))
|
||||
if run_count < 10: # let frame drops warm up
|
||||
frame_dropped_filter.x = 0.
|
||||
frames_dropped = 0.
|
||||
run_count = run_count + 1
|
||||
|
||||
frame_drop_ratio = frames_dropped / (1 + frames_dropped)
|
||||
prepare_only = vipc_dropped_frames > 0
|
||||
if prepare_only:
|
||||
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
|
||||
|
||||
bufs = {'road': buf_main, 'wide': buf_extra}
|
||||
transforms = {'road': model_transform_main, 'wide': model_transform_extra}
|
||||
|
||||
dt = 0.1
|
||||
d_yaw = yaw_rate * dt
|
||||
d_pos = v_ego * dt * np.array([np.cos(d_yaw/2), np.sin(d_yaw/2)])
|
||||
rot = np.array([[np.cos(-d_yaw), -np.sin(-d_yaw)], [np.sin(-d_yaw), np.cos(-d_yaw)]])
|
||||
modeld.history_buffer[:, :2] = (modeld.history_buffer[:, :2] - d_pos) @ rot.T
|
||||
modeld.history_buffer[:, 2] -= d_yaw
|
||||
modeld.history_buffer = np.roll(modeld.history_buffer, -1, axis=0)
|
||||
modeld.history_buffer[-1] = 0.
|
||||
|
||||
hist_input = modeld.history_buffer.copy()
|
||||
hist_input[:, 1] *= -1.0
|
||||
hist_input[:, 2] *= -1.0
|
||||
yaws_fixed = hist_input[:, 2]
|
||||
cos_y, sin_y = np.cos(yaws_fixed), np.sin(yaws_fixed)
|
||||
zeros, ones = np.zeros_like(cos_y), np.ones_like(cos_y)
|
||||
rot_flat = np.column_stack([cos_y, -sin_y, zeros, sin_y, cos_y, zeros, zeros, zeros, ones])
|
||||
|
||||
inputs = {
|
||||
'input_ids': input_id_helper.update(sm),
|
||||
'history': np.column_stack([hist_input[:, :2], zeros, rot_flat])[None, ...].astype(np.float32),
|
||||
'logic_pulse': modeld.logic_pulse,
|
||||
'traffic_convention': np.array([[0.0, 1.0]] if sm["driverMonitoringState"].isRHD else [[1.0, 0.0]], dtype=np.float32),
|
||||
'v_ego': v_ego
|
||||
}
|
||||
|
||||
t0 = time.monotonic()
|
||||
outputs = modeld.run(bufs, transforms, inputs, prepare_only)
|
||||
t1 = time.monotonic()
|
||||
if not prepare_only:
|
||||
cloudlog.warning(f"Modeld: Inference took {(t1-t0)*1000:.2f} ms")
|
||||
last_vipc_frame_id = meta_main.frame_id
|
||||
|
||||
if outputs is not None:
|
||||
modeld.logic_pulse[:] = outputs["pred_pulse"]
|
||||
model_msg = messaging.new_message('modelV2')
|
||||
drivingdata_msg = messaging.new_message('drivingModelData')
|
||||
posenet_msg = messaging.new_message('cameraOdometry')
|
||||
|
||||
fill_alpamayo_msg(model_msg.modelV2, outputs, meta_main.frame_id, frame_drop_ratio, meta_main.timestamp_eof, CP, lat_delay, v_ego)
|
||||
model_msg.valid = live_calib_seen and (vipc_dropped_frames < 1)
|
||||
|
||||
fill_pose_msg(posenet_msg.cameraOdometry, outputs, meta_main.frame_id, meta_main.timestamp_eof)
|
||||
posenet_msg.valid = live_calib_seen and (vipc_dropped_frames < 1)
|
||||
|
||||
drivingdata_msg.drivingModelData.frameId = meta_main.frame_id
|
||||
|
||||
pm.send('drivingModelData', drivingdata_msg)
|
||||
pm.send('cameraOdometry', posenet_msg)
|
||||
pm.send('modelV2', model_msg)
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
sunnypilot/modeld_sunny/tests/__init__.py
Normal file
0
sunnypilot/modeld_sunny/tests/__init__.py
Normal file
83
sunnypilot/modeld_sunny/tests/test_integration.py
Normal file
83
sunnypilot/modeld_sunny/tests/test_integration.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
from cereal import car
|
||||
import cereal.messaging as messaging
|
||||
from msgq.visionipc import VisionIpcServer, VisionIpcClient, VisionStreamType
|
||||
from sunnypilot.modeld_sunny.modeld import AlpamayoModelD
|
||||
from sunnypilot.modeld_v2.models.commonmodel_pyx import CLContext
|
||||
from sunnypilot.modeld_sunny.fill_model_msg import fill_alpamayo_msg
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cl_context():
|
||||
return CLContext()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def modeld(cl_context):
|
||||
print("Initializing AlpamayoModelD...")
|
||||
return AlpamayoModelD(cl_context)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def vipc_server():
|
||||
server_name = "camerad_test"
|
||||
server = VisionIpcServer(server_name)
|
||||
server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 1, 1024, 512)
|
||||
server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 1, 1024, 512)
|
||||
server.start_listener()
|
||||
yield server
|
||||
|
||||
|
||||
def test_modeld(cl_context, modeld, vipc_server):
|
||||
v_ego = 20.0
|
||||
inputs = {
|
||||
'input_ids': np.zeros((1, 16), dtype=np.int64),
|
||||
'history': np.zeros((1, 16, 12), dtype=np.float32),
|
||||
'logic_pulse': modeld.logic_pulse,
|
||||
'traffic_convention': np.array([[1.0, 0.0]], dtype=np.float32),
|
||||
'v_ego': v_ego
|
||||
}
|
||||
|
||||
server_name = "camerad_test"
|
||||
client_road = VisionIpcClient(server_name, VisionStreamType.VISION_STREAM_ROAD, False, cl_context)
|
||||
assert client_road.connect(True), "Road client failed to connect"
|
||||
client_wide = VisionIpcClient(server_name, VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context)
|
||||
assert client_wide.connect(True), "Wide client failed to connect"
|
||||
|
||||
# NV12 size for 1024x512 = 1024*512 * 1.5 = 786432
|
||||
yuv_data = b'\x00' * 786432
|
||||
vipc_server.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data)
|
||||
vipc_server.send(VisionStreamType.VISION_STREAM_WIDE_ROAD, yuv_data)
|
||||
buf_road = client_road.recv()
|
||||
buf_wide = client_wide.recv()
|
||||
assert buf_road is not None
|
||||
assert buf_wide is not None
|
||||
|
||||
bufs = {'road': buf_road, 'wide': buf_wide}
|
||||
transforms = {'road': np.eye(3, dtype=np.float32), 'wide': np.eye(3, dtype=np.float32)}
|
||||
outputs = modeld.run(bufs, transforms, inputs, False)
|
||||
|
||||
assert outputs is not None
|
||||
assert outputs["position"].shape == (1, 64, 3)
|
||||
assert outputs["velocity"].shape == (1, 64, 3)
|
||||
assert outputs["acceleration"].shape == (1, 64, 3)
|
||||
assert outputs["orientation"].shape == (1, 64, 3)
|
||||
assert "pred_pulse" in outputs
|
||||
assert "pred_light" in outputs
|
||||
assert "pred_lead" in outputs
|
||||
|
||||
assert np.all(np.isfinite(outputs["position"])), "Position contains NaN/Inf"
|
||||
assert np.all(np.isfinite(outputs["velocity"])), "Velocity contains NaN/Inf"
|
||||
assert "consistency_error" in outputs
|
||||
assert outputs["consistency_error"] >= 0.0
|
||||
|
||||
model = messaging.new_message('modelV2')
|
||||
CP = car.CarParams.new_message()
|
||||
CP.longitudinalActuatorDelay = 0.2
|
||||
fill_alpamayo_msg(model.modelV2, outputs, 12345, 0.0, 1e9, CP, 0.1, v_ego)
|
||||
# these just ensure that the model should outputs same action for the same black pixels
|
||||
assert model.modelV2.action.desiredAcceleration == pytest.approx(-6.75, abs=1e-2)
|
||||
assert model.modelV2.action.desiredCurvature == pytest.approx(-0.05, abs=1e-2)
|
||||
assert not model.modelV2.action.shouldStop
|
||||
assert model.modelV2.frameId == 12345
|
||||
@@ -60,7 +60,7 @@ def tg_compile(flags, model_name):
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
flags = {
|
||||
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0',
|
||||
'larch64': 'DEV=QCOM',
|
||||
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
|
||||
tg_compile(flags, model_name)
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from openpilot.system.hardware import TICI
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['DEV'] = 'AMD'
|
||||
os.environ['AMD_IFACE'] = 'USB'
|
||||
import time
|
||||
import numpy as np
|
||||
import cereal.messaging as messaging
|
||||
@@ -73,7 +66,7 @@ class ModelState(ModelStateBase):
|
||||
self.PLANPLUS_CONTROL: float = 1.0
|
||||
|
||||
buffer_length = 5 if self.model_runner.is_20hz else 2
|
||||
self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names}
|
||||
self.frames = {name: DrivingModelFrame(context, 512, 256, buffer_length) for name in self.model_runner.vision_input_names}
|
||||
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
|
||||
|
||||
# img buffers are managed in openCL transform code
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
|
||||
#include "common/clutil.h"
|
||||
|
||||
DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length) : ModelFrame(device_id, context), buffer_length(buffer_length) {
|
||||
DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, int width, int height, uint8_t buffer_length) : ModelFrame(device_id, context), MODEL_WIDTH(width), MODEL_HEIGHT(height), buffer_length(buffer_length) {
|
||||
MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2;
|
||||
buf_size = MODEL_FRAME_SIZE * 2;
|
||||
frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t);
|
||||
|
||||
input_frames = std::make_unique<uint8_t[]>(buf_size);
|
||||
input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err));
|
||||
img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buffer_length*frame_size_bytes, NULL, &err));
|
||||
|
||||
@@ -64,15 +64,15 @@ protected:
|
||||
|
||||
class DrivingModelFrame : public ModelFrame {
|
||||
public:
|
||||
DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length);
|
||||
DrivingModelFrame(cl_device_id device_id, cl_context context, int width, int height, uint8_t buffer_length);
|
||||
~DrivingModelFrame();
|
||||
cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection);
|
||||
|
||||
const int MODEL_WIDTH = 512;
|
||||
const int MODEL_HEIGHT = 256;
|
||||
const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2;
|
||||
const int buf_size = MODEL_FRAME_SIZE * 2;
|
||||
const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t);
|
||||
int MODEL_WIDTH;
|
||||
int MODEL_HEIGHT;
|
||||
int MODEL_FRAME_SIZE;
|
||||
int buf_size;
|
||||
size_t frame_size_bytes;
|
||||
const uint8_t buffer_length;
|
||||
|
||||
private:
|
||||
|
||||
@@ -20,7 +20,7 @@ cdef extern from "sunnypilot/modeld_v2/models/commonmodel.h":
|
||||
|
||||
cppclass DrivingModelFrame:
|
||||
int buf_size
|
||||
DrivingModelFrame(cl_device_id, cl_context, unsigned char)
|
||||
DrivingModelFrame(cl_device_id, cl_context, int, int, unsigned char)
|
||||
|
||||
cppclass MonitoringModelFrame:
|
||||
int buf_size
|
||||
|
||||
@@ -59,8 +59,8 @@ cdef class ModelFrame:
|
||||
cdef class DrivingModelFrame(ModelFrame):
|
||||
cdef cppDrivingModelFrame * _frame
|
||||
|
||||
def __cinit__(self, CLContext context, int buffer_length=2):
|
||||
self._frame = new cppDrivingModelFrame(context.device_id, context.context, buffer_length)
|
||||
def __cinit__(self, CLContext context, int width, int height, int buffer_length=2):
|
||||
self._frame = new cppDrivingModelFrame(context.device_id, context.context, width, height, buffer_length)
|
||||
self.frame = <cppModelFrame*>(self._frame)
|
||||
self.buf_size = self._frame.buf_size
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class ModelCache:
|
||||
|
||||
class ModelFetcher:
|
||||
"""Handles fetching and caching of model data from remote source"""
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v14.json"
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v11.json"
|
||||
|
||||
def __init__(self, params: Params):
|
||||
self.params = params
|
||||
|
||||
@@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths
|
||||
from pathlib import Path
|
||||
|
||||
# see the README.md for more details on the model selector versioning
|
||||
CURRENT_SELECTOR_VERSION = 14
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 14
|
||||
CURRENT_SELECTOR_VERSION = 13
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 12
|
||||
|
||||
USE_ONNX = os.getenv('USE_ONNX', PC)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import os
|
||||
from abc import abstractmethod, ABC
|
||||
|
||||
import numpy as np
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, CLMemDict, FrameDict, Model, SliceDict, SEND_RAW_PRED
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
import pickle
|
||||
@@ -9,6 +11,15 @@ import pickle
|
||||
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
|
||||
|
||||
# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['DEV'] = 'AMD'
|
||||
os.environ['AMD_IFACE'] = 'USB'
|
||||
else:
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||
|
||||
|
||||
class ModelData:
|
||||
"""
|
||||
Stores metadata and configuration for a specific machine learning model.
|
||||
|
||||
@@ -51,7 +51,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
|
||||
self.input_to_dtype = {}
|
||||
self.input_to_device = {}
|
||||
for idx, name in enumerate(self.model_run.captured.expected_names):
|
||||
info = self.model_run.captured.expected_input_info[idx]
|
||||
info = self.model_run.captured.expected_st_vars_dtype_device[idx]
|
||||
self.input_to_dtype[name] = info[2] # dtype
|
||||
self.input_to_device[name] = info[3] # device
|
||||
|
||||
@@ -84,7 +84,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
|
||||
|
||||
def _run_model(self) -> NumpyDict:
|
||||
"""Runs the Tinygrad model inference and parses the outputs."""
|
||||
outputs = self.model_run(**self.inputs).numpy().flatten()
|
||||
outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
return self._parse_outputs(outputs)
|
||||
|
||||
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||
|
||||
@@ -17,16 +17,13 @@ class BlinkerPauseLateral:
|
||||
self.enabled = self.params.get_bool("BlinkerPauseLateralControl")
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.min_speed = 0
|
||||
self.reengage_delay = 0
|
||||
self.blinker_off_timer = 0.0
|
||||
|
||||
def get_params(self) -> None:
|
||||
self.enabled = self.params.get_bool("BlinkerPauseLateralControl")
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.min_speed = self.params.get("BlinkerMinLateralControlSpeed", return_default=True)
|
||||
self.reengage_delay = self.params.get("BlinkerLateralReengageDelay", return_default=True)
|
||||
self.min_speed = self.params.get("BlinkerMinLateralControlSpeed")
|
||||
|
||||
def update(self, CS: car.CarState, DT_CTRL: float = 0.01) -> bool:
|
||||
def update(self, CS: car.CarState) -> bool:
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
@@ -34,11 +31,4 @@ class BlinkerPauseLateral:
|
||||
speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
|
||||
min_speed_ms = self.min_speed * speed_factor
|
||||
|
||||
below_speed = CS.vEgo < min_speed_ms
|
||||
|
||||
if one_blinker and below_speed:
|
||||
self.blinker_off_timer = self.reengage_delay
|
||||
elif self.blinker_off_timer > 0:
|
||||
self.blinker_off_timer -= DT_CTRL
|
||||
|
||||
return bool((one_blinker and below_speed) or self.blinker_off_timer > 0)
|
||||
return bool(one_blinker and CS.vEgo < min_speed_ms)
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 pytest
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal import custom, log
|
||||
@@ -13,52 +12,11 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision
|
||||
|
||||
VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
|
||||
|
||||
|
||||
def _th_above_f32(th: float) -> float:
|
||||
"""
|
||||
Return the next representable float32 *above* `th`.
|
||||
This avoids flaky comparisons around thresholds due to float32 rounding.
|
||||
"""
|
||||
th32 = np.float32(th)
|
||||
above32 = np.nextafter(th32, np.float32(np.inf), dtype=np.float32)
|
||||
return float(above32)
|
||||
|
||||
|
||||
def _build_single_spike_filtered(n: int, base: float = 1.0) -> np.ndarray:
|
||||
"""
|
||||
Create an array where max() is >= threshold but p97 is < threshold.
|
||||
This demonstrates the behavior difference vs np.amax().
|
||||
|
||||
Note: We intentionally construct using float32-representable values to match
|
||||
the data path through cereal/capnp.
|
||||
"""
|
||||
th = float(_ENTERING_PRED_LAT_ACC_TH)
|
||||
th32 = float(np.float32(th))
|
||||
|
||||
# numpy percentile default is linear interpolation: idx=(n-1)*p/100
|
||||
idx = (n - 1) * 0.97
|
||||
w = float(idx - np.floor(idx))
|
||||
|
||||
base32 = float(np.float32(base))
|
||||
|
||||
# Choose spike so that p97 = base + w*(spike-base) < th
|
||||
# -> spike < base + (th-base)/w. Use a margin (0.9) and ensure spike >= th.
|
||||
if w == 0.0:
|
||||
spike = th32 + 1.0
|
||||
else:
|
||||
spike = base32 + (th32 - base32) / w * 0.9
|
||||
spike = max(spike, th32 + 0.01)
|
||||
|
||||
arr = np.full(n, base32, dtype=np.float32)
|
||||
arr[-1] = np.float32(spike)
|
||||
return arr
|
||||
|
||||
|
||||
def generate_modelV2():
|
||||
model = messaging.new_message('modelV2')
|
||||
position = log.XYZTData.new_message()
|
||||
@@ -143,72 +101,4 @@ class TestSmartCruiseControlVision:
|
||||
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
|
||||
assert self.scc_v.state == VisionState.enabled
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"case, should_enter",
|
||||
[
|
||||
("p97_just_above_threshold", True),
|
||||
("single_spike_filtered", False),
|
||||
("persistent_high_values", True),
|
||||
],
|
||||
ids=[
|
||||
"p97>threshold_enters",
|
||||
"single_spike_max_large_but_p97_below_threshold",
|
||||
"high_values_persist_trigger_entering",
|
||||
],
|
||||
)
|
||||
def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter):
|
||||
n = len(ModelConstants.T_IDXS)
|
||||
th = float(_ENTERING_PRED_LAT_ACC_TH)
|
||||
|
||||
if case == "p97_just_above_threshold":
|
||||
# Use the next representable float32 above threshold to avoid float32 rounding flakiness.
|
||||
val = _th_above_f32(th)
|
||||
pred_lat_accels = np.full(n, np.float32(val), dtype=np.float32)
|
||||
|
||||
elif case == "single_spike_filtered":
|
||||
pred_lat_accels = _build_single_spike_filtered(n, base=1.0)
|
||||
|
||||
elif case == "persistent_high_values":
|
||||
# Make enough "high" samples so p97 is driven by the persistent trend, not a single outlier.
|
||||
high_count = max(2, int(np.ceil(n * 0.03)) + 1)
|
||||
pred_lat_accels = np.full(n, np.float32(1.0), dtype=np.float32)
|
||||
pred_lat_accels[-high_count:] = np.float32(2.0)
|
||||
pred_lat_accels[-1] = np.float32(8.0) # keep one big outlier too
|
||||
|
||||
else:
|
||||
raise AssertionError(f"Unknown case: {case}")
|
||||
|
||||
# Override model predictions so:
|
||||
# predicted_lat_accels = abs(orientationRate.z) * velocity.x == pred_lat_accels
|
||||
mdl = generate_modelV2()
|
||||
mdl.modelV2.velocity.x = [1.0 for _ in range(n)]
|
||||
mdl.modelV2.orientationRate.z = [float(x) for x in pred_lat_accels]
|
||||
self.sm["modelV2"] = mdl.modelV2
|
||||
|
||||
v_ego = float(MIN_V + 5.0)
|
||||
|
||||
# 1st update: disabled -> enabled
|
||||
self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0)
|
||||
# 2nd update: evaluate entering condition from enabled state
|
||||
self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0)
|
||||
|
||||
# Controller does percentile on numpy float64 arrays (values already quantized by capnp),
|
||||
# so compute expected in float64 to match behavior and avoid interpolation/rounding deltas.
|
||||
expected_p97 = float(np.percentile(pred_lat_accels.astype(np.float64), 97))
|
||||
|
||||
# allow tiny numeric differences due to float conversions/interpolation
|
||||
assert np.isclose(self.scc_v.max_pred_lat_acc, expected_p97, rtol=1e-6, atol=1e-5)
|
||||
|
||||
if should_enter:
|
||||
# We assert entering primarily by state (this is the actual intended behavior).
|
||||
assert self.scc_v.state == VisionState.entering
|
||||
# Optional sanity: should be >= threshold with some margin (since we used nextafter above threshold).
|
||||
assert self.scc_v.max_pred_lat_acc > th
|
||||
|
||||
else:
|
||||
# Difference vs np.amax(): max can be above threshold, but p97 stays below it.
|
||||
assert float(np.max(pred_lat_accels)) >= th
|
||||
assert self.scc_v.max_pred_lat_acc < th
|
||||
assert self.scc_v.state == VisionState.enabled
|
||||
|
||||
# TODO-SP: mock modelV2 data to test other states
|
||||
|
||||
@@ -90,7 +90,7 @@ class SmartCruiseControlVision:
|
||||
|
||||
# get the maximum lat accel from the model
|
||||
predicted_lat_accels = rate_plan * vel_plan
|
||||
self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97)
|
||||
self.max_pred_lat_acc = np.amax(predicted_lat_accels)
|
||||
|
||||
# get the maximum curve based on the current velocity
|
||||
v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user