mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-02-18 20:03:53 +08:00
Merge branch 'master' into ui-better-confidence-ball
This commit is contained in:
39
.github/workflows/jenkins-pr-trigger.yaml
vendored
39
.github/workflows/jenkins-pr-trigger.yaml
vendored
@@ -5,7 +5,44 @@ on:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
# TODO: gc old branches in a separate job in this workflow
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan-comments:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request }}
|
||||
|
||||
17
.github/workflows/repo-maintenance.yaml
vendored
17
.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@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
|
||||
with:
|
||||
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
commit-message: "Update translations"
|
||||
@@ -48,6 +48,12 @@ jobs:
|
||||
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 '*'
|
||||
@@ -61,7 +67,7 @@ jobs:
|
||||
python selfdrive/car/docs.py
|
||||
git add docs/CARS.md
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
|
||||
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 }}
|
||||
@@ -70,5 +76,10 @@ jobs:
|
||||
branch: auto-package-updates
|
||||
base: master
|
||||
delete-branch: true
|
||||
body: 'Automatic PR from repo-maintenance -> package_updates'
|
||||
body: |
|
||||
Automatic PR from repo-maintenance -> package_updates
|
||||
|
||||
```
|
||||
${{ steps.pip_tree.outputs.PIP_TREE }}
|
||||
```
|
||||
labels: bot
|
||||
|
||||
27
.github/workflows/tests.yaml
vendored
27
.github/workflows/tests.yaml
vendored
@@ -20,8 +20,6 @@ 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
|
||||
|
||||
@@ -217,12 +215,13 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .ci_cache/comma_download_cache
|
||||
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }}
|
||||
key: proc-replay-${{ hashFiles('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"
|
||||
@@ -236,10 +235,26 @@ jobs:
|
||||
with:
|
||||
name: process_replay_diff.txt
|
||||
path: selfdrive/test/process_replay/diff.txt
|
||||
- name: Upload reference logs
|
||||
if: false # TODO: move this to github instead of azure
|
||||
- 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
|
||||
run: |
|
||||
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
|
||||
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
|
||||
- name: Run regen
|
||||
if: false
|
||||
timeout-minutes: 4
|
||||
|
||||
51
.github/workflows/vendor_third_party.yaml
vendored
Normal file
51
.github/workflows/vendor_third_party.yaml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
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,6 +100,7 @@ Pipfile
|
||||
.ionide
|
||||
|
||||
.claude/
|
||||
.context/
|
||||
PLAN.md
|
||||
TASK.md
|
||||
|
||||
|
||||
@@ -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'], LIBS=[msgq, common, 'pthread'])
|
||||
env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc', 'messaging/bridge_zmq.cc'], LIBS=[msgq, common, 'pthread'])
|
||||
|
||||
socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc'])
|
||||
|
||||
|
||||
@@ -1478,6 +1478,11 @@ 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 {
|
||||
@@ -2227,9 +2232,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<ZMQPoller>();
|
||||
auto pub_context = std::make_unique<MSGQContext>();
|
||||
auto sub_context = std::make_unique<ZMQContext>();
|
||||
std::map<SubSocket *, PubSocket *> sub2pub;
|
||||
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;
|
||||
|
||||
for (auto endpoint : endpoints) {
|
||||
auto pub_sock = new MSGQPubSocket();
|
||||
auto sub_sock = new ZMQSubSocket();
|
||||
auto pub_sock = new PubSocket();
|
||||
auto sub_sock = new BridgeZmqSubSocket();
|
||||
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);
|
||||
|
||||
170
cereal/messaging/bridge_zmq.cc
Normal file
170
cereal/messaging/bridge_zmq.cc
Normal file
@@ -0,0 +1,170 @@
|
||||
#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;
|
||||
}
|
||||
72
cereal/messaging/bridge_zmq.h
Normal file
72
cereal/messaging/bridge_zmq.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#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<ZMQContext>();
|
||||
msgq_context = std::make_unique<MSGQContext>();
|
||||
zmq_context = std::make_unique<BridgeZmqContext>();
|
||||
msgq_context = std::make_unique<Context>();
|
||||
|
||||
// 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<ZMQPubSocket>();
|
||||
socket_pair.pub_sock = std::make_unique<BridgeZmqPubSocket>();
|
||||
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
|
||||
ZMQPubSocket *pub_sock = sub2pub.at(sub_sock);
|
||||
BridgeZmqPubSocket *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->sock, addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED);
|
||||
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), 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->sock, nullptr, 0);
|
||||
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), nullptr, 0);
|
||||
zmq_close(pollitems[i].socket);
|
||||
}
|
||||
cv.notify_one();
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define private public
|
||||
#include "msgq/impl_msgq.h"
|
||||
#include "msgq/impl_zmq.h"
|
||||
#include "cereal/messaging/bridge_zmq.h"
|
||||
|
||||
class MsgqToZmq {
|
||||
public:
|
||||
@@ -22,16 +21,16 @@ protected:
|
||||
|
||||
struct SocketPair {
|
||||
std::string endpoint;
|
||||
std::unique_ptr<ZMQPubSocket> pub_sock;
|
||||
std::unique_ptr<BridgeZmqPubSocket> pub_sock;
|
||||
std::unique_ptr<MSGQSubSocket> sub_sock;
|
||||
int connected_clients = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<MSGQContext> msgq_context;
|
||||
std::unique_ptr<ZMQContext> zmq_context;
|
||||
std::unique_ptr<Context> msgq_context;
|
||||
std::unique_ptr<BridgeZmqContext> zmq_context;
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::unique_ptr<MSGQPoller> msgq_poller;
|
||||
std::map<SubSocket *, ZMQPubSocket *> sub2pub;
|
||||
std::map<SubSocket *, BridgeZmqPubSocket *> sub2pub;
|
||||
std::vector<SocketPair> socket_pairs;
|
||||
};
|
||||
|
||||
@@ -137,6 +137,7 @@ 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"}},
|
||||
|
||||
@@ -167,6 +167,92 @@ 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: 20f2493855...4c4e814ed5
Submodule opendbc_repo updated: ff2f9686c2...f07b9b3f38
2
panda
2
panda
Submodule panda updated: ed8a6f9ec2...a95e060e85
@@ -89,7 +89,6 @@ testing = [
|
||||
"pytest-timeout",
|
||||
"pytest-asyncio",
|
||||
"pytest-mock",
|
||||
"pytest-repeat",
|
||||
"ruff",
|
||||
"codespell",
|
||||
"pre-commit-hooks",
|
||||
@@ -97,15 +96,12 @@ 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 = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
||||
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
||||
|
||||
|
||||
def _languages():
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9df44871e9f5fa910622b0b92205b92a54d137dbdc3827b92e8622d85ff2e08e
|
||||
size 5189
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:013b368b38b17d9b2ef6aaf0f498f672deed95888084b7287f42bdfba617cbb6
|
||||
size 10142
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8fd563eec78d5ce4a8204c2f596789e1090cb3e26a35b4ffeacee4ab61968538
|
||||
size 8303
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0be8d5eddcd9f87acbf1daccf446be6218522120f64aee1ee0a3c0b31560f076
|
||||
size 15761
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af8d5ecb6468442361462aa838a2d234b1256b8139418be8ef2962e4350cfbef
|
||||
size 2176
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43b64365a42d7bf772d567b8867a6ced4ec0175bb88b6acaa3a5345f19ca696e
|
||||
size 1268
|
||||
3
selfdrive/assets/icons_mici/settings/keyboard/enter.png
Normal file
3
selfdrive/assets/icons_mici/settings/keyboard/enter.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dd956d5ccfce01a01bea74ef59c9e73dfca406a5ff9ac62417203afa6027fba
|
||||
size 5620
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1dd1c2308872729d58adab390030ae9c987dc7908f0c39391651ea2b6cb620c5
|
||||
size 2445
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88e6c50358f627fc714c1e9883143aeed00baabeab16132e16001aa1051e5eb8
|
||||
size 1272
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fce940a3cbd2e9530e8efdde90794013a272919b2f3ea482bc06535c795640e7
|
||||
size 2176
|
||||
@@ -22,7 +22,8 @@ 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")
|
||||
|
||||
SOURCES = ['lead0', 'lead1', 'cruise', 'e2e']
|
||||
LongitudinalPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
|
||||
MPC_SOURCES = (LongitudinalPlanSource.lead0, LongitudinalPlanSource.lead1, LongitudinalPlanSource.cruise)
|
||||
|
||||
X_DIM = 3
|
||||
U_DIM = 1
|
||||
@@ -107,10 +108,10 @@ def gen_long_model():
|
||||
a_min = SX.sym('a_min')
|
||||
a_max = SX.sym('a_max')
|
||||
x_obstacle = SX.sym('x_obstacle')
|
||||
prev_a = SX.sym('prev_a')
|
||||
a_prev = SX.sym('a_prev')
|
||||
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, prev_a, lead_t_follow, lead_danger_factor)
|
||||
model.p = vertcat(a_min, a_max, x_obstacle, a_prev, lead_t_follow, lead_danger_factor)
|
||||
|
||||
# dynamics model
|
||||
f_expl = vertcat(v_ego, a_ego, j_ego)
|
||||
@@ -142,7 +143,7 @@ def gen_long_ocp():
|
||||
|
||||
a_min, a_max = ocp.model.p[0], ocp.model.p[1]
|
||||
x_obstacle = ocp.model.p[2]
|
||||
prev_a = ocp.model.p[3]
|
||||
a_prev = ocp.model.p[3]
|
||||
lead_t_follow = ocp.model.p[4]
|
||||
lead_danger_factor = ocp.model.p[5]
|
||||
|
||||
@@ -159,7 +160,7 @@ def gen_long_ocp():
|
||||
x_ego,
|
||||
v_ego,
|
||||
a_ego,
|
||||
a_ego - prev_a,
|
||||
a_ego - a_prev,
|
||||
j_ego]
|
||||
ocp.model.cost_y_expr = vertcat(*costs)
|
||||
ocp.model.cost_y_expr_e = vertcat(*costs[:-1])
|
||||
@@ -217,7 +218,7 @@ class LongitudinalMpc:
|
||||
self.dt = dt
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = SOURCES[2]
|
||||
self.source = LongitudinalPlanSource.cruise
|
||||
|
||||
def reset(self):
|
||||
self.solver.reset()
|
||||
@@ -227,7 +228,7 @@ class LongitudinalMpc:
|
||||
self.v_solution = np.zeros(N+1)
|
||||
self.a_solution = np.zeros(N+1)
|
||||
self.j_solution = np.zeros(N)
|
||||
self.prev_a = np.array(self.a_solution)
|
||||
self.a_prev = np.array(self.a_solution)
|
||||
self.yref = np.zeros((N+1, COST_DIM))
|
||||
|
||||
for i in range(N):
|
||||
@@ -335,7 +336,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 = SOURCES[np.argmin(x_obstacles[0])]
|
||||
self.source = MPC_SOURCES[np.argmin(x_obstacles[0])]
|
||||
|
||||
self.yref[:,:] = 0.0
|
||||
for i in range(N):
|
||||
@@ -345,7 +346,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.prev_a)
|
||||
self.params[:,3] = np.copy(self.a_prev)
|
||||
self.params[:,4] = t_follow
|
||||
self.params[:,5] = LEAD_DANGER_FACTOR
|
||||
|
||||
@@ -377,7 +378,7 @@ class LongitudinalMpc:
|
||||
self.a_solution = self.x_sol[:,2]
|
||||
self.j_solution = self.u_sol[:,0]
|
||||
|
||||
self.prev_a = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
|
||||
self.a_prev = 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, SOURCES
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource
|
||||
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 = SOURCES[3]
|
||||
self.mpc.source = LongitudinalPlanSource.e2e
|
||||
else:
|
||||
output_a_target = output_a_target_mpc
|
||||
self.output_should_stop = output_should_stop_mpc
|
||||
|
||||
238
selfdrive/debug/mem_usage.py
Executable file
238
selfdrive/debug/mem_usage.py
Executable file
@@ -0,0 +1,238 @@
|
||||
#!/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)
|
||||
@@ -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).contiguous().realize().uop.base.buffer.numpy()
|
||||
output = self.model_run(**self.tensor_inputs).numpy().flatten()
|
||||
|
||||
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).contiguous().realize().uop.base.buffer.numpy()
|
||||
self.policy_output = self.policy_run(**self.policy_inputs).numpy().flatten()
|
||||
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}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:35e4a5d4c4d481f915e42358af4665b2c92b8f5c1efd1c0731f21b876ad1d856
|
||||
size 6954249
|
||||
oid sha256:3446bf8b22e50e47669a25bf32460ae8baf8547037f346753e19ecbfcf6d4e59
|
||||
size 6954368
|
||||
|
||||
@@ -35,7 +35,14 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._EYE_THRESHOLD = 0.65
|
||||
self._SG_THRESHOLD = 0.9
|
||||
self._BLINK_THRESHOLD = 0.865
|
||||
self._PHONE_THRESH = 0.5
|
||||
|
||||
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._POSE_PITCH_THRESHOLD = 0.3133
|
||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||
@@ -145,10 +152,11 @@ 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 = []
|
||||
@@ -249,7 +257,12 @@ 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 > self.settings._PHONE_THRESH:
|
||||
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:
|
||||
distracted_types.append(DistractedType.DISTRACTED_PHONE)
|
||||
|
||||
return distracted_types
|
||||
@@ -288,7 +301,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
|
||||
@@ -302,9 +315,11 @@ 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:
|
||||
@@ -410,6 +425,8 @@ 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, FW_PATH
|
||||
from panda import Panda, PandaDFU, PandaProtocolMismatch, McuType, 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(panda: Panda) -> bytes:
|
||||
def get_expected_signature() -> bytes:
|
||||
try:
|
||||
fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn)
|
||||
fn = os.path.join(FW_PATH, McuType.H7.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(panda)
|
||||
fw_signature = get_expected_signature()
|
||||
internal_panda = panda.is_internal()
|
||||
|
||||
panda_version = "bootstub" if panda.bootstub else panda.get_version()
|
||||
|
||||
@@ -304,11 +304,6 @@ 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"),
|
||||
},
|
||||
|
||||
|
||||
@@ -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] [--upload-only]
|
||||
[--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs]
|
||||
Regression test to identify changes in a process's output
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -33,7 +33,6 @@ 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 tabulate import tabulate
|
||||
from openpilot.common.utils import tabulate
|
||||
|
||||
from openpilot.common.git import get_commit
|
||||
from openpilot.system.hardware import PC
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
67f3daf309dc6cbb6844fcbaeb83e6596637e551
|
||||
@@ -9,14 +9,13 @@ 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, upload_file
|
||||
from openpilot.tools.lib.openpilotci import get_url
|
||||
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
|
||||
|
||||
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
||||
from openpilot.tools.lib.url_file import URLFile
|
||||
|
||||
source_segments = [
|
||||
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
|
||||
@@ -66,46 +65,17 @@ segments = [
|
||||
# dashcamOnly makes don't need to be tested until a full port is done
|
||||
excluded_interfaces = ["mock", "body", "psa"]
|
||||
|
||||
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
||||
BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/"
|
||||
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
|
||||
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)
|
||||
|
||||
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)
|
||||
return (segment, cfg.proc_name, res)
|
||||
|
||||
|
||||
@@ -144,27 +114,6 @@ 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}
|
||||
@@ -186,10 +135,6 @@ 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()
|
||||
@@ -199,33 +144,21 @@ 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 upload:
|
||||
if args.update_refs:
|
||||
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:
|
||||
print("Couldn't find reference commit")
|
||||
sys.exit(1)
|
||||
ref_commit = URLFile(BASE_URL + "ref_commit", cache=False).read().decode().strip()
|
||||
|
||||
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
|
||||
@@ -235,12 +168,11 @@ 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:
|
||||
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
|
||||
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:
|
||||
@@ -255,15 +187,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")
|
||||
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst".replace("|", "_"))
|
||||
if args.update_refs: # reference logs will not exist if routes were just regenerated
|
||||
ref_log_path = get_url(*segment.rsplit("--", 1,), "rlog.zst")
|
||||
route, seg_num = segment.rsplit("--", 1)
|
||||
ref_log_path = get_url(route, seg_num, "rlog.zst")
|
||||
else:
|
||||
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst")
|
||||
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst".replace("|", "_"))
|
||||
ref_log_path = ref_log_fn if os.path.exists(ref_log_fn) else BASE_URL + os.path.basename(ref_log_fn)
|
||||
|
||||
dat = None if args.upload_only else log_data[segment]
|
||||
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, dat))
|
||||
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, log_data[segment]))
|
||||
|
||||
log_paths[segment][cfg.proc_name]['ref'] = ref_log_path
|
||||
log_paths[segment][cfg.proc_name]['new'] = cur_log_fn
|
||||
@@ -271,19 +203,16 @@ 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)):
|
||||
if not args.upload_only:
|
||||
results[segment][proc] = result
|
||||
results[segment][proc] = result
|
||||
|
||||
diff_short, diff_long, failed = format_diff(results, log_paths, ref_commit)
|
||||
if not upload:
|
||||
if not args.update_refs:
|
||||
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")
|
||||
|
||||
@@ -292,8 +221,4 @@ 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 tabulate import tabulate
|
||||
from openpilot.common.utils 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": 3.0,
|
||||
"system.proclogd": 7.0,
|
||||
"system.logmessaged": 1.0,
|
||||
"system.tombstoned": 0,
|
||||
"system.journald": 1.0,
|
||||
@@ -282,9 +282,12 @@ 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)
|
||||
], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False)
|
||||
self._scroller.set_reset_scroll_at_show(False)
|
||||
|
||||
# Disable scrolling when onroad is interacting with bookmark
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import threading
|
||||
import json
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
@@ -11,7 +10,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 BigMultiOptionDialog, BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import 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
|
||||
@@ -121,6 +120,9 @@ 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")
|
||||
@@ -222,7 +224,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 to respond")
|
||||
self.set_value("updater failed\nto respond")
|
||||
self._state = UpdaterState.IDLE
|
||||
self._hide_value_t = rl.get_time()
|
||||
|
||||
@@ -303,30 +305,14 @@ 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 camera preview", "", "icons_mici/settings/device/cameras.png")
|
||||
driver_cam_btn = BigButton("driver\ncamera 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 training guide", "", "icons_mici/settings/device/info.png")
|
||||
review_training_guide_btn = BigButton("review\ntraining 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())
|
||||
|
||||
@@ -353,7 +339,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, callback=setattr(self, '_fcc_dialog', None))
|
||||
gui_app.set_modal_overlay(self._fcc_dialog)
|
||||
|
||||
def _offroad_transition(self):
|
||||
self._power_off_btn.set_visible(ui_state.is_offroad())
|
||||
@@ -371,10 +357,6 @@ 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
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigCircleToggle
|
||||
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.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
@@ -39,8 +39,7 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigCircleToggle("icons_mici/tethering_short.png", toggle_callback=tethering_toggle_callback,
|
||||
icon_size=(82, 82), icon_offset=(0, 12))
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
@@ -56,9 +55,6 @@ 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)
|
||||
@@ -74,8 +70,13 @@ 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)
|
||||
|
||||
wifi_button = BigButton("wi-fi")
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
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))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
@@ -90,7 +91,7 @@ class NetworkLayoutMici(NavWidget):
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_button,
|
||||
self._wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
@@ -99,7 +100,6 @@ class NetworkLayoutMici(NavWidget):
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set initial config
|
||||
@@ -158,8 +158,22 @@ 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 IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
# 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 network metered
|
||||
self._network_metered_btn.set_value(
|
||||
|
||||
@@ -50,12 +50,16 @@ 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 = round(self._network.strength / 100 * 2)
|
||||
strength = self.get_strength_icon_idx(self._network.strength)
|
||||
if strength == 2:
|
||||
strength_icon = self._wifi_full_txt
|
||||
elif strength == 1:
|
||||
@@ -314,7 +318,7 @@ class WifiUIMici(BigMultiOptionDialog):
|
||||
INACTIVITY_TIMEOUT = 1
|
||||
|
||||
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
|
||||
super().__init__([], None, None, right_btn_callback=None)
|
||||
super().__init__([], None)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
@@ -30,22 +30,27 @@ 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 = BigButton("toggles", "", "icons_mici/settings.png")
|
||||
toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png")
|
||||
toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES))
|
||||
network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
|
||||
network_btn = SettingsBigButton("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 = BigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
|
||||
device_btn = SettingsBigButton("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 = BigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
|
||||
developer_btn = SettingsBigButton("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 = BigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
|
||||
firehose_btn = SettingsBigButton("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
|
||||
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
||||
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,6 +169,7 @@ 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'])
|
||||
@@ -221,8 +222,11 @@ class AugmentedRoadView(CameraView):
|
||||
# Draw all UI overlays
|
||||
self._model_renderer.render(self._content_rect)
|
||||
|
||||
# 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)
|
||||
# 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)))
|
||||
|
||||
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
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP, ModelRendererSP
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
@@ -51,9 +51,10 @@ class LeadVehicle:
|
||||
fill_alpha: int = 0
|
||||
|
||||
|
||||
class ModelRenderer(Widget):
|
||||
class ModelRenderer(Widget, ModelRendererSP):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
Widget.__init__(self)
|
||||
ModelRendererSP.__init__(self)
|
||||
self._longitudinal_control = False
|
||||
self._experimental_mode = False
|
||||
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
|
||||
@@ -340,6 +341,10 @@ class ModelRenderer(Widget):
|
||||
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,9 +3,8 @@ 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 MiciLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
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
|
||||
|
||||
@@ -18,6 +17,7 @@ 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,6 +52,12 @@ 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
|
||||
@@ -65,10 +71,7 @@ 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)
|
||||
|
||||
# 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)
|
||||
self._draw_content(btn_y)
|
||||
|
||||
|
||||
class BigCircleToggle(BigCircleButton):
|
||||
@@ -93,48 +96,41 @@ class BigCircleToggle(BigCircleButton):
|
||||
if self._toggle_callback:
|
||||
self._toggle_callback(self._checked)
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
|
||||
# draw status icon
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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)):
|
||||
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64),
|
||||
scroll: bool = False):
|
||||
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_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._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._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
|
||||
|
||||
@@ -149,28 +145,33 @@ 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) < 12:
|
||||
font_size = 64
|
||||
elif len(self.text) < 17:
|
||||
font_size = 48
|
||||
elif len(self.text) < 20:
|
||||
font_size = 42
|
||||
if len(self.text) <= 18:
|
||||
return 48
|
||||
else:
|
||||
font_size = 36
|
||||
return 42
|
||||
|
||||
def _update_label_layout(self):
|
||||
self._label.set_font_size(self._get_label_font_size())
|
||||
if self.value:
|
||||
font_size -= 20
|
||||
|
||||
return font_size
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
else:
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
|
||||
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
|
||||
@@ -178,37 +179,35 @@ class BigButton(Widget):
|
||||
def get_text(self):
|
||||
return self.text
|
||||
|
||||
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()
|
||||
def _draw_content(self, btn_y: float):
|
||||
# LABEL ------------------------------------------------------------------
|
||||
label_x = self._rect.x + LABEL_HORIZONTAL_PADDING
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
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.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
|
||||
# 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)
|
||||
|
||||
def _render(self, _):
|
||||
# draw _txt_default_bg
|
||||
@@ -223,33 +222,7 @@ 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)
|
||||
|
||||
# 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)
|
||||
self._draw_content(btn_y)
|
||||
|
||||
|
||||
class BigToggle(BigButton):
|
||||
@@ -258,8 +231,6 @@ 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)
|
||||
@@ -277,15 +248,15 @@ class BigToggle(BigButton):
|
||||
def _draw_pill(self, x: float, y: float, checked: bool):
|
||||
# draw toggle icon top right
|
||||
if checked:
|
||||
rl.draw_texture(self._txt_enabled_toggle, int(x), int(y), rl.WHITE)
|
||||
rl.draw_texture_ex(self._txt_enabled_toggle, (x, y), 0, 1.0, rl.WHITE)
|
||||
else:
|
||||
rl.draw_texture(self._txt_disabled_toggle, int(x), int(y), rl.WHITE)
|
||||
rl.draw_texture_ex(self._txt_disabled_toggle, (x, y), 0, 1.0, rl.WHITE)
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
|
||||
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
|
||||
y = self._rect.y
|
||||
y = btn_y
|
||||
self._draw_pill(x, y, self._checked)
|
||||
|
||||
|
||||
@@ -297,15 +268,10 @@ 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 _get_label_font_size(self):
|
||||
font_size = super()._get_label_font_size()
|
||||
return font_size - 6
|
||||
def _width_hint(self) -> int:
|
||||
return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
@@ -315,13 +281,14 @@ class BigMultiToggle(BigToggle):
|
||||
if self._select_callback:
|
||||
self._select_callback(self.value)
|
||||
|
||||
def _render(self, _):
|
||||
BigButton._render(self, _)
|
||||
def _draw_content(self, btn_y: float):
|
||||
# don't draw pill from BigToggle
|
||||
BigButton._draw_content(self, btn_y)
|
||||
|
||||
checked_idx = self._options.index(self.value)
|
||||
|
||||
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
|
||||
y = self._rect.y
|
||||
y = btn_y
|
||||
|
||||
for i in range(len(self._options)):
|
||||
self._draw_pill(x, y, checked_idx == i)
|
||||
|
||||
@@ -14,7 +14,6 @@ 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
|
||||
|
||||
@@ -22,32 +21,17 @@ PADDING = 20
|
||||
|
||||
|
||||
class BigDialogBase(NavWidget, abc.ABC):
|
||||
def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None):
|
||||
def __init__(self):
|
||||
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
|
||||
|
||||
@@ -55,10 +39,8 @@ class BigDialogBase(NavWidget, abc.ABC):
|
||||
class BigDialog(BigDialogBase):
|
||||
def __init__(self,
|
||||
title: str,
|
||||
description: str,
|
||||
right_btn: str | None = None,
|
||||
right_btn_callback: Callable | None = None):
|
||||
super().__init__(right_btn, right_btn_callback)
|
||||
description: str):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._description = description
|
||||
|
||||
@@ -70,8 +52,6 @@ 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)
|
||||
@@ -139,7 +119,7 @@ class BigInputDialog(BigDialogBase):
|
||||
default_text: str = "",
|
||||
minimum_length: int = 1,
|
||||
confirm_callback: Callable[[str], None] | None = None):
|
||||
super().__init__(None, None)
|
||||
super().__init__()
|
||||
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()
|
||||
@@ -151,7 +131,8 @@ 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/confirm.png", 42, 36)
|
||||
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_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
# rects for top buttons
|
||||
@@ -186,9 +167,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 + bg_block_margin
|
||||
text_x = PADDING / 2 + self._enter_img.width + PADDING
|
||||
text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin,
|
||||
int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width) - bg_block_margin * 2,
|
||||
int(self._rect.width - text_x * 2),
|
||||
int(text_size.y))
|
||||
|
||||
# draw text input
|
||||
@@ -224,7 +205,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._enter_img.width - 15), int(text_field_rect.y), color)
|
||||
rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(self._rect.y + 14), 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
|
||||
@@ -236,10 +217,12 @@ 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)
|
||||
|
||||
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)
|
||||
# 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)
|
||||
|
||||
# keyboard goes over everything
|
||||
self._keyboard.render(self._rect)
|
||||
@@ -307,9 +290,8 @@ class BigDialogOptionButton(Widget):
|
||||
class BigMultiOptionDialog(BigDialogBase):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
|
||||
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)
|
||||
def __init__(self, options: list[str], default: str | None):
|
||||
super().__init__()
|
||||
self._options = options
|
||||
if default is not None:
|
||||
assert default in options
|
||||
@@ -322,8 +304,6 @@ 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))
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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,27 +4,190 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
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.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.")
|
||||
|
||||
|
||||
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):
|
||||
self._scroller.render(rect)
|
||||
if self._current_panel == PanelType.SLA:
|
||||
self._speed_limit_layout.render(rect)
|
||||
else:
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
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,6 +72,15 @@ 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"),
|
||||
@@ -96,6 +105,7 @@ 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,
|
||||
@@ -128,6 +138,7 @@ 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,6 +9,7 @@ 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
|
||||
@@ -112,7 +113,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", 2)
|
||||
ui_state.params.put("MadsSteeringMode", MadsSteeringModeOnBrake.DISENGAGE)
|
||||
|
||||
self._main_cruise_toggle.action_item.set_enabled(False)
|
||||
self._main_cruise_toggle.action_item.set_state(False)
|
||||
@@ -122,9 +123,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(2)
|
||||
self._steering_mode.action_item.set_selected_button(MadsSteeringModeOnBrake.DISENGAGE)
|
||||
self._steering_mode.action_item.set_enabled_buttons({MadsSteeringModeOnBrake.DISENGAGE})
|
||||
else:
|
||||
self._main_cruise_toggle.action_item.set_enabled(True)
|
||||
self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC)
|
||||
@@ -133,3 +134,4 @@ 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)
|
||||
|
||||
@@ -6,8 +6,14 @@ 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 fade_alpha > 1e-2:
|
||||
if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState' 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),
|
||||
|
||||
@@ -72,72 +72,28 @@ class TurnSignalWidget(Widget):
|
||||
|
||||
|
||||
class TurnSignalController:
|
||||
def __init__(self, config: TurnSignalConfig | None = None):
|
||||
self._config = config or TurnSignalConfig()
|
||||
def __init__(self):
|
||||
self._config = TurnSignalConfig()
|
||||
self._left_signal = TurnSignalWidget(direction=IconSide.left)
|
||||
self._right_signal = TurnSignalWidget(direction=IconSide.right)
|
||||
self._last_icon_side = None
|
||||
|
||||
@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()
|
||||
|
||||
def update(self):
|
||||
sm = ui_state.sm
|
||||
ss = sm['selfdriveState']
|
||||
CS = ui_state.sm['carState']
|
||||
|
||||
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()
|
||||
self._update_signal(self._left_signal, CS.leftBlindspot, CS.leftBlinker)
|
||||
self._update_signal(self._right_signal, CS.rightBlindspot, CS.rightBlinker)
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
if not ui_state.turn_signals:
|
||||
if not ui_state.turn_signals and not ui_state.blindspot:
|
||||
return
|
||||
|
||||
x = rect.x + rect.width / 2
|
||||
|
||||
@@ -39,6 +39,9 @@ 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:
|
||||
@@ -121,6 +124,7 @@ 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")
|
||||
@@ -138,16 +142,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)
|
||||
self.flat_confidence_ball = self.params.get_bool("FlatConfidenceBall")
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
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_awake(on: bool, _ui_state):
|
||||
if _ui_state.boot_offroad_mode == 1 and not on:
|
||||
_ui_state.params.put_bool("OffroadMode", True)
|
||||
|
||||
@staticmethod
|
||||
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
|
||||
|
||||
@@ -3,7 +3,6 @@ import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import base64
|
||||
import webbrowser
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
@@ -25,12 +24,6 @@ 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...")
|
||||
@@ -60,20 +53,16 @@ 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)
|
||||
|
||||
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)
|
||||
return different_frames, len(frames1)
|
||||
|
||||
|
||||
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
|
||||
def generate_html_report(video1, video2, basedir, different_frames, total_frames):
|
||||
chunks = []
|
||||
if different_frames:
|
||||
current_chunk = [different_frames[0]]
|
||||
@@ -177,14 +166,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, frame_data, total_frames = find_differences(args.video1, args.video2)
|
||||
different_frames, 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, frame_data, total_frames)
|
||||
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames)
|
||||
|
||||
with open(DIFF_OUT_DIR / args.output, 'w') as f:
|
||||
f.write(html)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@
|
||||
"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(self, on)
|
||||
DeviceSP._set_awake(on, ui_state)
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
|
||||
@@ -17,13 +17,16 @@ 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")
|
||||
self.min_speed = self.params.get("BlinkerMinLateralControlSpeed", return_default=True)
|
||||
self.reengage_delay = self.params.get("BlinkerLateralReengageDelay", return_default=True)
|
||||
|
||||
def update(self, CS: car.CarState) -> bool:
|
||||
def update(self, CS: car.CarState, DT_CTRL: float = 0.01) -> bool:
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
@@ -31,4 +34,11 @@ 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
|
||||
|
||||
return bool(one_blinker and CS.vEgo < min_speed_ms)
|
||||
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)
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -12,11 +13,52 @@ 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.vision_controller import SmartCruiseControlVision
|
||||
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
|
||||
|
||||
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()
|
||||
@@ -101,4 +143,72 @@ 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.amax(predicted_lat_accels)
|
||||
self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -20,6 +20,8 @@ class TestBlinkerPauseLateral:
|
||||
self.blinker_pause_lateral.enabled = True
|
||||
self.blinker_pause_lateral.is_metric = False
|
||||
self.blinker_pause_lateral.min_speed = 20 # MPH
|
||||
self.blinker_pause_lateral.reengage_delay = 0
|
||||
self.blinker_pause_lateral.blinker_off_timer = 0.0
|
||||
|
||||
self.CS = car.CarState.new_message()
|
||||
self.CS.vEgo = 0
|
||||
@@ -46,6 +48,18 @@ class TestBlinkerPauseLateral:
|
||||
}
|
||||
self._test_should_blinker_pause_lateral(expected_results)
|
||||
|
||||
def test_reengage_delay(self):
|
||||
self.blinker_pause_lateral.reengage_delay = 2 # seconds
|
||||
self.CS.vEgo = 4.5 # ~10 MPH
|
||||
|
||||
expected_results = {
|
||||
(False, False): True,
|
||||
(True, False): True,
|
||||
(False, True): True,
|
||||
(True, True): False
|
||||
}
|
||||
self._test_should_blinker_pause_lateral(expected_results)
|
||||
|
||||
def test_above_min_speed_blinker(self):
|
||||
self.CS.vEgo = 13.4 # ~30 MPH
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@
|
||||
"title": "[TIZI/TICI only] Blind Spot Detection",
|
||||
"description": "Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported."
|
||||
},
|
||||
"BlinkerLateralReengageDelay": {
|
||||
"title": "Post-Blinker Delay",
|
||||
"description": "Delay before lateral control resumes after the turn signal ends."
|
||||
},
|
||||
"BlinkerMinLateralControlSpeed": {
|
||||
"title": "Blinker Min Lateral Control Speed",
|
||||
"description": ""
|
||||
|
||||
@@ -166,6 +166,7 @@ class SunnylinkState:
|
||||
|
||||
def _worker_thread(self) -> None:
|
||||
while self._running:
|
||||
self._sm.update()
|
||||
if self.is_connected():
|
||||
self._fetch_roles()
|
||||
self._fetch_users()
|
||||
|
||||
@@ -3,7 +3,7 @@ import pytest
|
||||
import time
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from tabulate import tabulate
|
||||
from openpilot.common.utils import tabulate
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal.services import SERVICE_LIST
|
||||
|
||||
@@ -115,6 +115,55 @@ def _parse_proc_stat(stat: str) -> ProcStat | None:
|
||||
cloudlog.exception("failed to parse /proc/<pid>/stat")
|
||||
return None
|
||||
|
||||
class SmapsData(TypedDict):
|
||||
pss: int # bytes
|
||||
pss_anon: int # bytes
|
||||
pss_shmem: int # bytes
|
||||
|
||||
|
||||
_SMAPS_KEYS = {b'Pss:', b'Pss_Anon:', b'Pss_Shmem:'}
|
||||
|
||||
# smaps_rollup (kernel 4.14+) is ideal but missing on some BSP kernels;
|
||||
# fall back to per-VMA smaps (any kernel). Pss_Anon/Pss_Shmem only in 5.x+.
|
||||
_smaps_path: str | None = None # auto-detected on first call
|
||||
|
||||
# per-VMA smaps is expensive (kernel walks page tables for every VMA).
|
||||
# cache results and only refresh every N cycles to keep CPU low.
|
||||
_smaps_cache: dict[int, SmapsData] = {}
|
||||
_smaps_cycle = 0
|
||||
_SMAPS_EVERY = 20 # refresh every 20th cycle (40s at 0.5Hz)
|
||||
|
||||
|
||||
def _read_smaps(pid: int) -> SmapsData:
|
||||
global _smaps_path
|
||||
try:
|
||||
if _smaps_path is None:
|
||||
_smaps_path = 'smaps_rollup' if os.path.exists(f'/proc/{pid}/smaps_rollup') else 'smaps'
|
||||
|
||||
result: SmapsData = {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0}
|
||||
with open(f'/proc/{pid}/{_smaps_path}', 'rb') as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0] in _SMAPS_KEYS:
|
||||
val = int(parts[1]) * 1024 # kB -> bytes
|
||||
if parts[0] == b'Pss:':
|
||||
result['pss'] += val
|
||||
elif parts[0] == b'Pss_Anon:':
|
||||
result['pss_anon'] += val
|
||||
elif parts[0] == b'Pss_Shmem:':
|
||||
result['pss_shmem'] += val
|
||||
return result
|
||||
except (FileNotFoundError, PermissionError, ProcessLookupError, OSError):
|
||||
return {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0}
|
||||
|
||||
|
||||
def _get_smaps_cached(pid: int) -> SmapsData:
|
||||
"""Return cached smaps data, refreshing every _SMAPS_EVERY cycles."""
|
||||
if _smaps_cycle == 0 or pid not in _smaps_cache:
|
||||
_smaps_cache[pid] = _read_smaps(pid)
|
||||
return _smaps_cache.get(pid, {'pss': 0, 'pss_anon': 0, 'pss_shmem': 0})
|
||||
|
||||
|
||||
class ProcExtra(TypedDict):
|
||||
pid: int
|
||||
name: str
|
||||
@@ -189,6 +238,13 @@ def build_proc_log_message(msg) -> None:
|
||||
for j, arg in enumerate(extra['cmdline']):
|
||||
cmdline[j] = arg
|
||||
|
||||
# smaps is expensive (kernel walks page tables); skip small processes, use cache
|
||||
if r['rss'] * PAGE_SIZE > 5 * 1024 * 1024:
|
||||
smaps = _get_smaps_cached(r['pid'])
|
||||
proc.memPss = smaps['pss']
|
||||
proc.memPssAnon = smaps['pss_anon']
|
||||
proc.memPssShmem = smaps['pss_shmem']
|
||||
|
||||
cpu_times = _cpu_times()
|
||||
cpu_list = pl.init('cpuTimes', len(cpu_times))
|
||||
for i, ct in enumerate(cpu_times):
|
||||
@@ -212,6 +268,9 @@ def build_proc_log_message(msg) -> None:
|
||||
pl.mem.inactive = mem_info["Inactive:"]
|
||||
pl.mem.shared = mem_info["Shmem:"]
|
||||
|
||||
global _smaps_cycle
|
||||
_smaps_cycle = (_smaps_cycle + 1) % _SMAPS_EVERY
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
pm = messaging.PubMaster(['procLog'])
|
||||
|
||||
@@ -16,7 +16,6 @@ TRANSLATIONS_DIR = UI_DIR.joinpath("translations")
|
||||
LANGUAGES_FILE = TRANSLATIONS_DIR.joinpath("languages.json")
|
||||
|
||||
UNIFONT_LANGUAGES = [
|
||||
"ar",
|
||||
"th",
|
||||
"zh-CHT",
|
||||
"zh-CHS",
|
||||
|
||||
@@ -73,8 +73,14 @@ class GuiScrollPanel2:
|
||||
|
||||
def _update_state(self, bounds_size: float, content_size: float) -> None:
|
||||
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
|
||||
if self._state == ScrollState.AUTO_SCROLL:
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
|
||||
if self._state == ScrollState.STEADY:
|
||||
# if we find ourselves out of bounds, scroll back in (from external layout dimension changes, etc.)
|
||||
if self.get_offset() > max_offset or self.get_offset() < min_offset:
|
||||
self._state = ScrollState.AUTO_SCROLL
|
||||
|
||||
elif self._state == ScrollState.AUTO_SCROLL:
|
||||
# simple exponential return if out of bounds
|
||||
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
|
||||
if out_of_bounds and self._handle_out_of_bounds:
|
||||
|
||||
@@ -631,7 +631,7 @@ class WifiManager:
|
||||
known_connections = self._get_connections()
|
||||
networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()]
|
||||
# sort with quantized strength to reduce jumping
|
||||
networks.sort(key=lambda n: (-n.is_connected, -round(n.strength / 100 * 2), n.ssid.lower()))
|
||||
networks.sort(key=lambda n: (-n.is_connected, -n.is_saved, -round(n.strength / 100 * 2), n.ssid.lower()))
|
||||
self._networks = networks
|
||||
|
||||
self._update_ipv4_address()
|
||||
|
||||
@@ -94,12 +94,12 @@ class NetworkConnectivityMonitor:
|
||||
class SetupState(IntEnum):
|
||||
GETTING_STARTED = 0
|
||||
NETWORK_SETUP = 1
|
||||
NETWORK_SETUP_CUSTOM_SOFTWARE = 8
|
||||
SOFTWARE_SELECTION = 2
|
||||
CUSTOM_SOFTWARE = 3
|
||||
DOWNLOADING = 4
|
||||
DOWNLOAD_FAILED = 5
|
||||
CUSTOM_SOFTWARE_WARNING = 6
|
||||
NETWORK_SETUP_CUSTOM_SOFTWARE = 2
|
||||
SOFTWARE_SELECTION = 3
|
||||
CUSTOM_SOFTWARE = 4
|
||||
DOWNLOADING = 5
|
||||
DOWNLOAD_FAILED = 6
|
||||
CUSTOM_SOFTWARE_WARNING = 7
|
||||
|
||||
|
||||
class StartPage(Widget):
|
||||
@@ -590,15 +590,9 @@ class Setup(Widget):
|
||||
def _custom_software_warning_back_button_callback(self):
|
||||
self._set_state(SetupState.SOFTWARE_SELECTION)
|
||||
|
||||
def _custom_software_warning_continue_button_callback(self):
|
||||
self._set_state(SetupState.CUSTOM_SOFTWARE)
|
||||
|
||||
def _getting_started_button_callback(self):
|
||||
self._set_state(SetupState.SOFTWARE_SELECTION)
|
||||
|
||||
def _software_selection_back_button_callback(self):
|
||||
self._set_state(SetupState.GETTING_STARTED)
|
||||
|
||||
def _software_selection_continue_button_callback(self):
|
||||
self.use_openpilot()
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
def get_highlighted_description(params, param_name: str, descriptions: list[str]) -> str:
|
||||
index = int(params.get(param_name, return_default=True))
|
||||
lines = []
|
||||
for i, desc in enumerate(descriptions):
|
||||
if i == index:
|
||||
lines.append(f"<b>{desc}</b>")
|
||||
else:
|
||||
lines.append(f"{desc}")
|
||||
|
||||
return "<br>".join(lines)
|
||||
|
||||
@@ -134,6 +134,10 @@ class MultipleButtonActionSP(MultipleButtonAction):
|
||||
if self.param_key:
|
||||
self.selected_button = int(self.params.get(self.param_key, return_default=True))
|
||||
self._anim_x: float | None = None
|
||||
self.enabled_buttons: set[int] | None = None
|
||||
|
||||
def set_enabled_buttons(self, indices: set[int] | None):
|
||||
self.enabled_buttons = indices
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
|
||||
@@ -171,10 +175,31 @@ class MultipleButtonActionSP(MultipleButtonAction):
|
||||
text_x = button_x + (self.button_width - text_size.x) / 2
|
||||
text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2
|
||||
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color)
|
||||
# Check individual button enabled state
|
||||
is_button_enabled = self.enabled and (self.enabled_buttons is None or i in self.enabled_buttons)
|
||||
current_text_color = text_color if is_button_enabled else style.MBC_DISABLED
|
||||
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, current_text_color)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
MultipleButtonAction._handle_mouse_release(self, mouse_pos)
|
||||
# Override parent method to check individual button enabled state
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
button_y = self._rect.y + (self._rect.height - style.BUTTON_HEIGHT) / 2
|
||||
for i, _ in enumerate(self.buttons):
|
||||
button_x = self._rect.x + i * self.button_width
|
||||
button_rect = rl.Rectangle(button_x, button_y, self.button_width, style.BUTTON_HEIGHT)
|
||||
|
||||
if rl.check_collision_point_rec(mouse_pos, button_rect):
|
||||
# Check if this specific button is enabled
|
||||
if self.enabled_buttons is not None and i not in self.enabled_buttons:
|
||||
return
|
||||
|
||||
self.selected_button = i
|
||||
if self.callback:
|
||||
self.callback(i)
|
||||
|
||||
if self.param_key:
|
||||
self.params.put(self.param_key, self.selected_button)
|
||||
|
||||
@@ -212,6 +237,12 @@ class ListItemSP(ListItem):
|
||||
content_width = int(self._rect.width - style.ITEM_PADDING * 2)
|
||||
self._rect.height = self.get_item_height(self._font, content_width)
|
||||
|
||||
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
|
||||
super().set_parent_rect(parent_rect)
|
||||
if self.description_visible:
|
||||
content_width = int(self._rect.width - style.ITEM_PADDING * 2)
|
||||
self._rect.height = self.get_item_height(self._font, content_width)
|
||||
|
||||
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
||||
height = super().get_item_height(font, max_width)
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ NAV_BAR_WIDTH = 205
|
||||
NAV_BAR_HEIGHT = 8
|
||||
|
||||
DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing
|
||||
DISMISS_TIME_SECONDS = 1.5
|
||||
DISMISS_TIME_SECONDS = 2.0
|
||||
|
||||
|
||||
class NavBar(Widget):
|
||||
@@ -242,6 +242,7 @@ class NavWidget(Widget, abc.ABC):
|
||||
self._pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1)
|
||||
self._playing_dismiss_animation = False
|
||||
self._trigger_animate_in = False
|
||||
self._nav_bar_show_time = 0.0
|
||||
self._back_enabled: bool | Callable[[], bool] = True
|
||||
self._nav_bar = NavBar()
|
||||
|
||||
@@ -330,6 +331,7 @@ class NavWidget(Widget, abc.ABC):
|
||||
if self._trigger_animate_in:
|
||||
self._pos_filter.x = self._rect.height
|
||||
self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT
|
||||
self._nav_bar_show_time = rl.get_time()
|
||||
self._trigger_animate_in = False
|
||||
|
||||
new_y = 0.0
|
||||
@@ -366,18 +368,24 @@ class NavWidget(Widget, abc.ABC):
|
||||
|
||||
if self.back_enabled:
|
||||
bar_x = self._rect.x + (self._rect.width - self._nav_bar.rect.width) / 2
|
||||
nav_bar_delayed = rl.get_time() - self._nav_bar_show_time < 0.4
|
||||
# User dragging or dismissing, nav bar follows NavWidget
|
||||
if self._back_button_start_pos is not None or self._playing_dismiss_animation:
|
||||
self._nav_bar_y_filter.x = NAV_BAR_MARGIN + self._pos_filter.x
|
||||
# Waiting to show
|
||||
elif nav_bar_delayed:
|
||||
self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT
|
||||
# Animate back to top
|
||||
else:
|
||||
self._nav_bar_y_filter.update(NAV_BAR_MARGIN)
|
||||
|
||||
self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x))
|
||||
self._nav_bar.render()
|
||||
|
||||
# draw black above widget when dismissing
|
||||
if self._rect.y > 0:
|
||||
rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK)
|
||||
|
||||
self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x))
|
||||
self._nav_bar.render()
|
||||
|
||||
return ret
|
||||
|
||||
def show_event(self):
|
||||
|
||||
@@ -15,7 +15,6 @@ ANIMATION_SCALE = 0.6
|
||||
MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds
|
||||
DO_ZOOM = False
|
||||
DO_JELLO = False
|
||||
SCROLL_BAR = False
|
||||
|
||||
|
||||
class LineSeparator(Widget):
|
||||
@@ -33,9 +32,52 @@ class LineSeparator(Widget):
|
||||
LINE_COLOR)
|
||||
|
||||
|
||||
class ScrollIndicator(Widget):
|
||||
HORIZONTAL_MARGIN = 4
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/horizontal_scroll_indicator.png", 96, 48)
|
||||
self._scroll_offset: float = 0.0
|
||||
self._content_size: float = 0.0
|
||||
self._viewport: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
def update(self, scroll_offset: float, content_size: float, viewport: rl.Rectangle) -> None:
|
||||
self._scroll_offset = scroll_offset
|
||||
self._content_size = content_size
|
||||
self._viewport = viewport
|
||||
|
||||
def _render(self, _):
|
||||
# scale indicator width based on content size
|
||||
indicator_w = float(np.interp(self._content_size, [1000, 3000], [300, 100]))
|
||||
|
||||
# position based on scroll ratio
|
||||
slide_range = self._viewport.width - indicator_w
|
||||
max_scroll = self._content_size - self._viewport.width
|
||||
scroll_ratio = -self._scroll_offset / max_scroll
|
||||
x = self._viewport.x + scroll_ratio * slide_range
|
||||
# don't bounce up when NavWidget shows
|
||||
y = max(self._viewport.y, 0) + self._viewport.height - self._txt_scroll_indicator.height / 2
|
||||
|
||||
# squeeze when overscrolling past edges
|
||||
dest_left = max(x, self._viewport.x)
|
||||
dest_right = min(x + indicator_w, self._viewport.x + self._viewport.width)
|
||||
dest_w = max(indicator_w / 2, dest_right - dest_left)
|
||||
|
||||
# keep within viewport after applying minimum width
|
||||
dest_left = min(dest_left, self._viewport.x + self._viewport.width - dest_w)
|
||||
dest_left = max(dest_left, self._viewport.x)
|
||||
|
||||
src_rec = rl.Rectangle(0, 0, self._txt_scroll_indicator.width, self._txt_scroll_indicator.height)
|
||||
dest_rec = rl.Rectangle(dest_left, y, dest_w, self._txt_scroll_indicator.height)
|
||||
rl.draw_texture_pro(self._txt_scroll_indicator, src_rec, dest_rec, rl.Vector2(0, 0), 0.0,
|
||||
rl.Color(255, 255, 255, int(255 * 0.45)))
|
||||
|
||||
|
||||
class Scroller(Widget):
|
||||
def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING,
|
||||
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING):
|
||||
line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING,
|
||||
scroll_indicator: bool = True):
|
||||
super().__init__()
|
||||
self._items: list[Widget] = []
|
||||
self._horizontal = horizontal
|
||||
@@ -65,7 +107,8 @@ class Scroller(Widget):
|
||||
self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items)
|
||||
self._scroll_enabled: bool | Callable[[], bool] = True
|
||||
|
||||
self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80)
|
||||
self._show_scroll_indicator = scroll_indicator
|
||||
self._scroll_indicator = ScrollIndicator()
|
||||
|
||||
for item in items:
|
||||
self.add_widget(item)
|
||||
@@ -241,15 +284,13 @@ class Scroller(Widget):
|
||||
else:
|
||||
item.render()
|
||||
|
||||
# Draw scroll indicator
|
||||
if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0:
|
||||
_real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height
|
||||
scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height
|
||||
scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height)
|
||||
rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE)
|
||||
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Draw scroll indicator
|
||||
if self._show_scroll_indicator and self._horizontal and len(self._visible_items) > 0:
|
||||
self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect)
|
||||
self._scroll_indicator.render()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
if self._reset_scroll_at_show:
|
||||
|
||||
6
third_party/acados/.gitignore
vendored
6
third_party/acados/.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
acados_repo/
|
||||
lib
|
||||
/lib
|
||||
!x86_64/
|
||||
!larch64/
|
||||
!aarch64/
|
||||
!Darwin/
|
||||
!*.so
|
||||
!*.so.*
|
||||
!*.dylib
|
||||
|
||||
1
third_party/acados/acados_template/gnsf/__init__.py
vendored
Normal file
1
third_party/acados/acados_template/gnsf/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
third_party/acados/build.sh
vendored
3
third_party/acados/build.sh
vendored
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export SOURCE_DATE_EPOCH=0
|
||||
export ZERO_AR_DATE=1
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||
|
||||
ARCHNAME="x86_64"
|
||||
|
||||
4
third_party/acados/x86_64/lib/libacados.so
vendored
4
third_party/acados/x86_64/lib/libacados.so
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:821ce18f417d211c4845b60482d465b809f90dc7d04f023d652d8221e87679b1
|
||||
size 553544
|
||||
oid sha256:05a1ba3cf37fa929cdd56f892608b2f89c35a05ef1b07fedb86b2f0d76607263
|
||||
size 540488
|
||||
|
||||
4
third_party/acados/x86_64/lib/libblasfeo.so
vendored
4
third_party/acados/x86_64/lib/libblasfeo.so
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3feea7927d004064bbc5a13c3287467669ce801cb0a3c616cf9e089816da5a0b
|
||||
size 2155088
|
||||
oid sha256:c0bf22898d9c59b672d3d0961f5f4c804b9957478125d99eb297de3091bedd15
|
||||
size 2416112
|
||||
|
||||
4
third_party/acados/x86_64/lib/libhpipm.so
vendored
4
third_party/acados/x86_64/lib/libhpipm.so
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a042716f515913786581dff39799eb71fc66caddfa18b1c9f0d54f00c1568fd2
|
||||
size 1572648
|
||||
oid sha256:5b6875fb47940764d4ebb916c2373cb0e04929229feb654b290676c28d48fa9d
|
||||
size 1531024
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a6abea4815e3f03cff06fe8a9602e97f9acf102f18f803571460a94595b93be4
|
||||
size 262824
|
||||
oid sha256:04be908c3f707e5c968022b9cdd79ab75ae7af46e7fa019ceee98f854ddd3f64
|
||||
size 262464
|
||||
|
||||
4
third_party/acados/x86_64/t_renderer
vendored
4
third_party/acados/x86_64/t_renderer
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7a360d4b53826b91ada3358156d44a14d497bdd8ace88707fd4b386ed6d194c7
|
||||
size 17503920
|
||||
oid sha256:a53ae46650c4df5b0ddb87a658f59a0422e41743e8bc2d822da0aefd1d280791
|
||||
size 5088536
|
||||
|
||||
53
third_party/build.sh
vendored
Executable file
53
third_party/build.sh
vendored
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||
|
||||
# Reproducible builds: pin timestamps to epoch
|
||||
export SOURCE_DATE_EPOCH=0
|
||||
export ZERO_AR_DATE=1
|
||||
|
||||
pids=()
|
||||
names=()
|
||||
logs=()
|
||||
|
||||
for script in "$DIR"/*/build.sh; do
|
||||
[ -f "$script" ] || continue
|
||||
name=$(basename "$(dirname "$script")")
|
||||
log=$(mktemp)
|
||||
names+=("$name")
|
||||
logs+=("$log")
|
||||
(cd "$(dirname "$script")" && bash "$(basename "$script")") >"$log" 2>&1 &
|
||||
pids+=($!)
|
||||
done
|
||||
|
||||
failed=0
|
||||
for i in "${!pids[@]}"; do
|
||||
echo "--- ${names[$i]} ---"
|
||||
if wait "${pids[$i]}"; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAILED (exit $?)"
|
||||
failed=1
|
||||
fi
|
||||
cat "${logs[$i]}"
|
||||
rm -f "${logs[$i]}"
|
||||
echo
|
||||
done
|
||||
|
||||
[ $failed -ne 0 ] && exit $failed
|
||||
|
||||
# Repack ar archives with deterministic headers (zero timestamps/uid/gid)
|
||||
# Skip foreign-platform archives that ar can't read (e.g. Mach-O on Linux)
|
||||
while IFS= read -r -d '' lib; do
|
||||
tmpdir=$(mktemp -d)
|
||||
lib=$(realpath "$lib")
|
||||
if (cd "$tmpdir" && ar x "$lib" 2>/dev/null); then
|
||||
(cd "$tmpdir" && ar Drcs repacked.a * && mv repacked.a "$lib")
|
||||
fi
|
||||
rm -rf "$tmpdir"
|
||||
done < <(find "$DIR" -name '*.a' \
|
||||
\( -path '*/x86_64/*' -o -path '*/Darwin/*' -o -path '*/larch64/*' -o -path '*/aarch64/*' \) \
|
||||
-print0)
|
||||
|
||||
echo -e "\033[32mAll third_party builds succeeded.\033[0m"
|
||||
3
third_party/libyuv/.gitignore
vendored
3
third_party/libyuv/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
libyuv/
|
||||
/libyuv/
|
||||
!*.a
|
||||
|
||||
3
third_party/libyuv/build.sh
vendored
3
third_party/libyuv/build.sh
vendored
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export SOURCE_DATE_EPOCH=0
|
||||
export ZERO_AR_DATE=1
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||
|
||||
ARCHNAME=$(uname -m)
|
||||
|
||||
2
third_party/libyuv/larch64/lib/libyuv.a
vendored
2
third_party/libyuv/larch64/lib/libyuv.a
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:320bef5a75a62dd2731a496040921d5000f1ed237ae70fd7aeb6c010a1534363
|
||||
oid sha256:adafce26582e425164df7af36253ce58e3ed1dba9965650745c93bd96e42e976
|
||||
size 462482
|
||||
|
||||
1
third_party/libyuv/x86_64/include
vendored
1
third_party/libyuv/x86_64/include
vendored
@@ -1 +0,0 @@
|
||||
../include
|
||||
4
third_party/libyuv/x86_64/lib/libyuv.a
vendored
4
third_party/libyuv/x86_64/lib/libyuv.a
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e21a3bd8df01cf4ce5461e7bf6654239196036c3f829255145265c7bf31a791d
|
||||
size 511974
|
||||
oid sha256:00f9759c67c6fa21657fabde9e096478ea5809716989599f673f638f039431e5
|
||||
size 504790
|
||||
|
||||
1
third_party/raylib/.gitignore
vendored
1
third_party/raylib/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/raylib_repo/
|
||||
/raylib_python_repo/
|
||||
/wheel/
|
||||
!*.a
|
||||
|
||||
4
third_party/raylib/Darwin/libraylib.a
vendored
4
third_party/raylib/Darwin/libraylib.a
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7ffe1fc6497f0c111fc507988e94fd29ce4db53a4876dc82ab9267895ad82584
|
||||
size 6515352
|
||||
oid sha256:fd045c1d4bca5c9b2ad044ea730826ff6cedeef0b64451b123717b136f1cd702
|
||||
size 6392532
|
||||
|
||||
3
third_party/raylib/build.sh
vendored
3
third_party/raylib/build.sh
vendored
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export SOURCE_DATE_EPOCH=0
|
||||
export ZERO_AR_DATE=1
|
||||
|
||||
SUDO=""
|
||||
|
||||
# Use sudo if not root
|
||||
|
||||
2
third_party/raylib/larch64/libraylib.a
vendored
2
third_party/raylib/larch64/libraylib.a
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:91e9a07513e84f7b553da01b34b24e12fe7130131ef73ebdb3dac3b838db815b
|
||||
oid sha256:f760af8b4693cf60e3760341e5275890d78d933da2354c4bad0572ec575b970a
|
||||
size 2001860
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user