Merge branch 'master' into ui-better-confidence-ball

This commit is contained in:
royjr
2026-02-13 22:20:33 -05:00
108 changed files with 2252 additions and 2767 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View 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
View File

@@ -100,6 +100,7 @@ Pipfile
.ionide
.claude/
.context/
PLAN.md
TASK.md

View File

@@ -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'])

View File

@@ -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);

View File

@@ -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);

View 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;
}

View 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;
};

View File

@@ -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();

View File

@@ -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;
};

View File

@@ -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"}},

View File

@@ -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)

2
panda

Submodule panda updated: ed8a6f9ec2...a95e060e85

View File

@@ -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 = [

View File

@@ -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():

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9df44871e9f5fa910622b0b92205b92a54d137dbdc3827b92e8622d85ff2e08e
size 5189

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:013b368b38b17d9b2ef6aaf0f498f672deed95888084b7287f42bdfba617cbb6
size 10142

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8fd563eec78d5ce4a8204c2f596789e1090cb3e26a35b4ffeacee4ab61968538
size 8303

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0be8d5eddcd9f87acbf1daccf446be6218522120f64aee1ee0a3c0b31560f076
size 15761

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af8d5ecb6468442361462aa838a2d234b1256b8139418be8ef2962e4350cfbef
size 2176

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43b64365a42d7bf772d567b8867a6ced4ec0175bb88b6acaa3a5345f19ca696e
size 1268

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3dd956d5ccfce01a01bea74ef59c9e73dfca406a5ff9ac62417203afa6027fba
size 5620

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1dd1c2308872729d58adab390030ae9c987dc7908f0c39391651ea2b6cb620c5
size 2445

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88e6c50358f627fc714c1e9883143aeed00baabeab16132e16001aa1051e5eb8
size 1272

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fce940a3cbd2e9530e8efdde90794013a272919b2f3ea482bc06535c795640e7
size 2176

View File

@@ -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:

View File

@@ -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
View 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)

View File

@@ -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

View File

@@ -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}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:35e4a5d4c4d481f915e42358af4665b2c92b8f5c1efd1c0731f21b876ad1d856
size 6954249
oid sha256:3446bf8b22e50e47669a25bf32460ae8baf8547037f346753e19ecbfcf6d4e59
size 6954368

View File

@@ -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,

View File

@@ -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()

View File

@@ -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"),
},

View File

@@ -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

View File

@@ -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

View File

@@ -1 +0,0 @@
67f3daf309dc6cbb6844fcbaeb83e6596637e551

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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(

View File

@@ -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)

View File

@@ -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([

View File

@@ -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()

View File

@@ -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:

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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),

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -6,7 +6,6 @@
"Español": "es",
"Türkçe": "tr",
"Українська": "uk",
"العربية": "ar",
"ไทย": "th",
"中文(繁體)": "zh-CHT",
"中文(简体)": "zh-CHS",

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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": ""

View File

@@ -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()

View File

@@ -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

View File

@@ -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'])

View File

@@ -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",

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

@@ -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:

View File

@@ -1,5 +1,9 @@
acados_repo/
lib
/lib
!x86_64/
!larch64/
!aarch64/
!Darwin/
!*.so
!*.so.*
!*.dylib

View File

@@ -0,0 +1 @@

View File

@@ -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"

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:821ce18f417d211c4845b60482d465b809f90dc7d04f023d652d8221e87679b1
size 553544
oid sha256:05a1ba3cf37fa929cdd56f892608b2f89c35a05ef1b07fedb86b2f0d76607263
size 540488

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3feea7927d004064bbc5a13c3287467669ce801cb0a3c616cf9e089816da5a0b
size 2155088
oid sha256:c0bf22898d9c59b672d3d0961f5f4c804b9957478125d99eb297de3091bedd15
size 2416112

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a042716f515913786581dff39799eb71fc66caddfa18b1c9f0d54f00c1568fd2
size 1572648
oid sha256:5b6875fb47940764d4ebb916c2373cb0e04929229feb654b290676c28d48fa9d
size 1531024

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6abea4815e3f03cff06fe8a9602e97f9acf102f18f803571460a94595b93be4
size 262824
oid sha256:04be908c3f707e5c968022b9cdd79ab75ae7af46e7fa019ceee98f854ddd3f64
size 262464

View File

@@ -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
View 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"

View File

@@ -1 +1,2 @@
libyuv/
/libyuv/
!*.a

View File

@@ -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)

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:320bef5a75a62dd2731a496040921d5000f1ed237ae70fd7aeb6c010a1534363
oid sha256:adafce26582e425164df7af36253ce58e3ed1dba9965650745c93bd96e42e976
size 462482

View File

@@ -1 +0,0 @@
../include

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e21a3bd8df01cf4ce5461e7bf6654239196036c3f829255145265c7bf31a791d
size 511974
oid sha256:00f9759c67c6fa21657fabde9e096478ea5809716989599f673f638f039431e5
size 504790

View File

@@ -1,3 +1,4 @@
/raylib_repo/
/raylib_python_repo/
/wheel/
!*.a

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ffe1fc6497f0c111fc507988e94fd29ce4db53a4876dc82ab9267895ad82584
size 6515352
oid sha256:fd045c1d4bca5c9b2ad044ea730826ff6cedeef0b64451b123717b136f1cd702
size 6392532

View File

@@ -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

View File

@@ -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