Compare commits

..

2 Commits

Author SHA1 Message Date
discountchubbs
88dc1ac3bc Merge remote-tracking branch 'origin/master' into sunnymodel 2026-02-10 15:13:58 -08:00
discountchubbs
d7e5f3cf43 init sunnymodel with wiped locationd and livepose for temporary sim xytz data 2026-02-01 10:12:28 -08:00
143 changed files with 3718 additions and 2429 deletions

View File

@@ -140,7 +140,7 @@ jobs:
run: |
echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json
built=(); while IFS= read -r line; do built+=("$line"); done < <(
find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}'
)
jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \
'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json
@@ -168,7 +168,6 @@ jobs:
if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
model: ${{ fromJson(needs.setup.outputs.model_matrix) }}

View File

@@ -5,44 +5,7 @@ on:
types: [created, edited]
jobs:
cleanup-branches:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Delete stale Jenkins branches
uses: actions/github-script@v8
with:
script: |
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
const prefixes = ['tmp-jenkins', '__jenkins'];
for await (const response of github.paginate.iterator(github.rest.repos.listBranches, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
})) {
for (const branch of response.data) {
if (!prefixes.some(p => branch.name.startsWith(p))) continue;
const { data: commit } = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: branch.commit.sha,
});
const commitDate = new Date(commit.commit.committer.date).getTime();
if (commitDate < cutoff) {
console.log(`Deleting branch: ${branch.name} (last commit: ${commit.commit.committer.date})`);
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${branch.name}`,
});
}
}
}
# TODO: gc old branches in a separate job in this workflow
scan-comments:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request }}

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@c0f553fe549906ede9cf27b5156039d195d2ece0
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
commit-message: "Update translations"
@@ -43,22 +43,14 @@ jobs:
with:
submodules: true
- name: uv lock
if: github.repository == 'commaai/openpilot'
run: |
python3 -m ensurepip --upgrade
pip3 install uv
uv lock --upgrade
- name: uv pip tree
id: pip_tree
run: |
echo 'PIP_TREE<<EOF' >> $GITHUB_OUTPUT
uv pip tree >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: bump submodules
run: |
git config --global --add safe.directory '*'
git config submodule.msgq.update none
git config submodule.rednose_repo.update none
git config submodule.teleoprtc_repo.update none
git config submodule.tinygrad.update none
git submodule update --remote
git add .
@@ -69,7 +61,7 @@ jobs:
python selfdrive/car/docs.py
git add docs/CARS.md
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }}
@@ -78,10 +70,5 @@ jobs:
branch: auto-package-updates
base: master
delete-branch: true
body: |
Automatic PR from repo-maintenance -> package_updates
```
${{ steps.pip_tree.outputs.PIP_TREE }}
```
body: 'Automatic PR from repo-maintenance -> package_updates'
labels: bot

View File

@@ -20,6 +20,8 @@ concurrency:
env:
PYTHONWARNINGS: error
BASE_IMAGE: sunnypilot-base
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: release/ci/docker_build_sp.sh base
@@ -105,7 +107,6 @@ jobs:
build_mac:
name: build macOS
if: false # tmp disable due to brew install not working
runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
steps:
- uses: actions/checkout@v6
@@ -215,13 +216,12 @@ jobs:
uses: actions/cache@v5
with:
path: .ci_cache/comma_download_cache
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/test_processes.py') }}
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }}
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Run replay
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
continue-on-error: ${{ github.ref == 'refs/heads/master' }}
run: |
${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache"
@@ -235,26 +235,10 @@ jobs:
with:
name: process_replay_diff.txt
path: selfdrive/test/process_replay/diff.txt
- name: Checkout ci-artifacts
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
uses: actions/checkout@v4
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/ci-artifacts
- name: Push refs
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
working-directory: ${{ github.workspace }}/ci-artifacts
- name: Upload reference logs
if: false # TODO: move this to github instead of azure
run: |
git checkout --orphan process-replay
git rm -rf .
git config user.name "GitHub Actions Bot"
git config user.email "<>"
cp ${{ github.workspace }}/selfdrive/test/process_replay/fakedata/*.zst .
echo "${{ github.sha }}" > ref_commit
git add .
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}"
git push origin process-replay --force
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- name: Run regen
if: false
timeout-minutes: 4

View File

@@ -1,51 +0,0 @@
name: vendor third_party
on:
workflow_dispatch:
jobs:
build:
if: github.ref != 'refs/heads/master'
strategy:
matrix:
os: [ubuntu-24.04, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Build
run: third_party/build.sh
- name: Package artifacts
run: |
git add -A third_party/
git diff --cached --name-only -- third_party/ | tar -cf /tmp/third_party_build.tar -T -
- uses: actions/upload-artifact@v4
with:
name: third-party-${{ runner.os }}
path: /tmp/third_party_build.tar
commit:
needs: build
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
path: /tmp/artifacts
- name: Commit vendored libraries
run: |
for f in /tmp/artifacts/*/third_party_build.tar; do
tar xf "$f"
done
git add third_party/
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "third_party: rebuild vendor libraries"
git push

1
.gitignore vendored
View File

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

2
.gitmodules vendored
View File

@@ -15,7 +15,7 @@
url = https://github.com/commaai/teleoprtc
[submodule "tinygrad"]
path = tinygrad_repo
url = https://github.com/sunnypilot/tinygrad.git
url = https://github.com/commaai/tinygrad.git
[submodule "sunnypilot/neural_network_data"]
path = sunnypilot/neural_network_data
url = https://github.com/sunnypilot/neural-network-data.git

View File

@@ -9,6 +9,4 @@ WORKDIR ${OPENPILOT_PATH}
COPY . ${OPENPILOT_PATH}/
ENV UV_BIN="/home/batman/.local/bin/"
ENV PATH="$UV_BIN:$PATH"
RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc)
RUN scons --cache-readonly -j$(nproc)

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', 'messaging/bridge_zmq.cc'], LIBS=[msgq, common, 'pthread'])
env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc'], LIBS=[msgq, common, 'pthread'])
socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc'])

View File

@@ -1478,11 +1478,6 @@ struct ProcLog {
cmdline @15 :List(Text);
exe @16 :Text;
# from /proc/<pid>/smaps_rollup (proportional/private memory)
memPss @17 :UInt64; # Pss — shared pages split by mapper count
memPssAnon @18 :UInt64; # Pss_Anon — private anonymous (heap, stack)
memPssShmem @19 :UInt64; # Pss_Shmem — proportional MSGQ/tmpfs share
}
struct CPUTimes {
@@ -2232,9 +2227,9 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
isActiveMode @16 :Bool;
isRHD @4 :Bool;
uncertainCount @19 :UInt32;
phoneProbOffset @20 :Float32;
phoneProbValidCount @21 :UInt32;
phoneProbOffsetDEPRECATED @20 :Float32;
phoneProbValidCountDEPRECATED @21 :UInt32;
isPreviewDEPRECATED @15 :Bool;
rhdCheckedDEPRECATED @5 :Bool;
eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED);

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<BridgeZmqPoller>();
auto pub_context = std::make_unique<Context>();
auto sub_context = std::make_unique<BridgeZmqContext>();
std::map<BridgeZmqSubSocket *, PubSocket *> sub2pub;
auto poller = std::make_unique<ZMQPoller>();
auto pub_context = std::make_unique<MSGQContext>();
auto sub_context = std::make_unique<ZMQContext>();
std::map<SubSocket *, PubSocket *> sub2pub;
for (auto endpoint : endpoints) {
auto pub_sock = new PubSocket();
auto sub_sock = new BridgeZmqSubSocket();
auto pub_sock = new MSGQPubSocket();
auto sub_sock = new ZMQSubSocket();
size_t queue_size = services.at(endpoint).queue_size;
pub_sock->connect(pub_context.get(), endpoint, true, queue_size);
sub_sock->connect(sub_context.get(), endpoint, ip, false);

View File

@@ -1,170 +0,0 @@
#include "cereal/messaging/bridge_zmq.h"
#include <cassert>
#include <cstring>
#include <unistd.h>
static size_t fnv1a_hash(const std::string &str) {
const size_t fnv_prime = 0x100000001b3;
size_t hash_value = 0xcbf29ce484222325;
for (char c : str) {
hash_value ^= (unsigned char)c;
hash_value *= fnv_prime;
}
return hash_value;
}
// FIXME: This is a hack to get the port number from the socket name, might have collisions.
static int get_port(std::string endpoint) {
size_t hash_value = fnv1a_hash(endpoint);
int start_port = 8023;
int max_port = 65535;
return start_port + (hash_value % (max_port - start_port));
}
BridgeZmqContext::BridgeZmqContext() {
context = zmq_ctx_new();
}
BridgeZmqContext::~BridgeZmqContext() {
if (context != nullptr) {
zmq_ctx_term(context);
}
}
void BridgeZmqMessage::init(size_t sz) {
size = sz;
data = new char[size];
}
void BridgeZmqMessage::init(char *d, size_t sz) {
size = sz;
data = new char[size];
memcpy(data, d, size);
}
void BridgeZmqMessage::close() {
if (size > 0) {
delete[] data;
}
data = nullptr;
size = 0;
}
BridgeZmqMessage::~BridgeZmqMessage() {
close();
}
int BridgeZmqSubSocket::connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate, bool check_endpoint) {
sock = zmq_socket(context->getRawContext(), ZMQ_SUB);
if (sock == nullptr) {
return -1;
}
zmq_setsockopt(sock, ZMQ_SUBSCRIBE, "", 0);
if (conflate) {
int arg = 1;
zmq_setsockopt(sock, ZMQ_CONFLATE, &arg, sizeof(int));
}
int reconnect_ivl = 500;
zmq_setsockopt(sock, ZMQ_RECONNECT_IVL_MAX, &reconnect_ivl, sizeof(reconnect_ivl));
full_endpoint = "tcp://" + address + ":";
if (check_endpoint) {
full_endpoint += std::to_string(get_port(endpoint));
} else {
full_endpoint += endpoint;
}
return zmq_connect(sock, full_endpoint.c_str());
}
void BridgeZmqSubSocket::setTimeout(int timeout) {
zmq_setsockopt(sock, ZMQ_RCVTIMEO, &timeout, sizeof(int));
}
Message *BridgeZmqSubSocket::receive(bool non_blocking) {
zmq_msg_t msg;
assert(zmq_msg_init(&msg) == 0);
int flags = non_blocking ? ZMQ_DONTWAIT : 0;
int rc = zmq_msg_recv(&msg, sock, flags);
Message *ret = nullptr;
if (rc >= 0) {
ret = new BridgeZmqMessage;
ret->init((char *)zmq_msg_data(&msg), zmq_msg_size(&msg));
}
zmq_msg_close(&msg);
return ret;
}
BridgeZmqSubSocket::~BridgeZmqSubSocket() {
if (sock != nullptr) {
zmq_close(sock);
}
}
int BridgeZmqPubSocket::connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint) {
sock = zmq_socket(context->getRawContext(), ZMQ_PUB);
if (sock == nullptr) {
return -1;
}
full_endpoint = "tcp://*:";
if (check_endpoint) {
full_endpoint += std::to_string(get_port(endpoint));
} else {
full_endpoint += endpoint;
}
// ZMQ pub sockets cannot be shared between processes, so we need to ensure pid stays the same.
pid = getpid();
return zmq_bind(sock, full_endpoint.c_str());
}
int BridgeZmqPubSocket::sendMessage(Message *message) {
assert(pid == getpid());
return zmq_send(sock, message->getData(), message->getSize(), ZMQ_DONTWAIT);
}
int BridgeZmqPubSocket::send(char *data, size_t size) {
assert(pid == getpid());
return zmq_send(sock, data, size, ZMQ_DONTWAIT);
}
BridgeZmqPubSocket::~BridgeZmqPubSocket() {
if (sock != nullptr) {
zmq_close(sock);
}
}
void BridgeZmqPoller::registerSocket(BridgeZmqSubSocket *socket) {
assert(num_polls + 1 < (sizeof(polls) / sizeof(polls[0])));
polls[num_polls].socket = socket->getRawSocket();
polls[num_polls].events = ZMQ_POLLIN;
sockets.push_back(socket);
num_polls++;
}
std::vector<BridgeZmqSubSocket *> BridgeZmqPoller::poll(int timeout) {
std::vector<BridgeZmqSubSocket *> ret;
int rc = zmq_poll(polls, num_polls, timeout);
if (rc < 0) {
return ret;
}
for (size_t i = 0; i < num_polls; i++) {
if (polls[i].revents) {
ret.push_back(sockets[i]);
}
}
return ret;
}

View File

@@ -1,72 +0,0 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include <zmq.h>
#include "msgq/ipc.h"
class BridgeZmqContext {
public:
BridgeZmqContext();
void *getRawContext() { return context; }
~BridgeZmqContext();
private:
void *context = nullptr;
};
class BridgeZmqMessage : public Message {
public:
void init(size_t size);
void init(char *data, size_t size);
void close();
size_t getSize() { return size; }
char *getData() { return data; }
~BridgeZmqMessage();
private:
char *data = nullptr;
size_t size = 0;
};
class BridgeZmqSubSocket {
public:
int connect(BridgeZmqContext *context, std::string endpoint, std::string address, bool conflate = false, bool check_endpoint = true);
void setTimeout(int timeout);
Message *receive(bool non_blocking = false);
void *getRawSocket() { return sock; }
~BridgeZmqSubSocket();
private:
void *sock = nullptr;
std::string full_endpoint;
};
class BridgeZmqPubSocket {
public:
int connect(BridgeZmqContext *context, std::string endpoint, bool check_endpoint = true);
int sendMessage(Message *message);
int send(char *data, size_t size);
void *getRawSocket() { return sock; }
~BridgeZmqPubSocket();
private:
void *sock = nullptr;
std::string full_endpoint;
int pid = -1;
};
class BridgeZmqPoller {
public:
void registerSocket(BridgeZmqSubSocket *socket);
std::vector<BridgeZmqSubSocket *> poll(int timeout);
private:
static constexpr size_t MAX_BRIDGE_ZMQ_POLLERS = 128;
std::vector<BridgeZmqSubSocket *> sockets;
zmq_pollitem_t polls[MAX_BRIDGE_ZMQ_POLLERS] = {};
size_t num_polls = 0;
};

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<BridgeZmqContext>();
msgq_context = std::make_unique<Context>();
zmq_context = std::make_unique<ZMQContext>();
msgq_context = std::make_unique<MSGQContext>();
// Create ZMQPubSockets for each endpoint
for (const auto &endpoint : endpoints) {
auto &socket_pair = socket_pairs.emplace_back();
socket_pair.endpoint = endpoint;
socket_pair.pub_sock = std::make_unique<BridgeZmqPubSocket>();
socket_pair.pub_sock = std::make_unique<ZMQPubSocket>();
int ret = socket_pair.pub_sock->connect(zmq_context.get(), endpoint);
if (ret != 0) {
printf("Failed to create ZMQ publisher for [%s]: %s\n", endpoint.c_str(), zmq_strerror(zmq_errno()));
@@ -49,7 +49,7 @@ void MsgqToZmq::run(const std::vector<std::string> &endpoints, const std::string
for (auto sub_sock : msgq_poller->poll(100)) {
// Process messages for each socket
BridgeZmqPubSocket *pub_sock = sub2pub.at(sub_sock);
ZMQPubSocket *pub_sock = sub2pub.at(sub_sock);
for (int i = 0; i < MAX_MESSAGES_PER_SOCKET; ++i) {
auto msg = std::unique_ptr<Message>(sub_sock->receive(true));
if (!msg) break;
@@ -72,7 +72,7 @@ void MsgqToZmq::zmqMonitorThread() {
// Set up ZMQ monitor for each pub socket
for (int i = 0; i < socket_pairs.size(); ++i) {
std::string addr = "inproc://op-bridge-monitor-" + std::to_string(i);
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED);
zmq_socket_monitor(socket_pairs[i].pub_sock->sock, addr.c_str(), ZMQ_EVENT_ACCEPTED | ZMQ_EVENT_DISCONNECTED);
void *monitor_socket = zmq_socket(zmq_context->getRawContext(), ZMQ_PAIR);
zmq_connect(monitor_socket, addr.c_str());
@@ -130,7 +130,7 @@ void MsgqToZmq::zmqMonitorThread() {
// Clean up monitor sockets
for (int i = 0; i < pollitems.size(); ++i) {
zmq_socket_monitor(socket_pairs[i].pub_sock->getRawSocket(), nullptr, 0);
zmq_socket_monitor(socket_pairs[i].pub_sock->sock, nullptr, 0);
zmq_close(pollitems[i].socket);
}
cv.notify_one();

View File

@@ -7,8 +7,9 @@
#include <string>
#include <vector>
#define private public
#include "msgq/impl_msgq.h"
#include "cereal/messaging/bridge_zmq.h"
#include "msgq/impl_zmq.h"
class MsgqToZmq {
public:
@@ -21,16 +22,16 @@ protected:
struct SocketPair {
std::string endpoint;
std::unique_ptr<BridgeZmqPubSocket> pub_sock;
std::unique_ptr<ZMQPubSocket> pub_sock;
std::unique_ptr<MSGQSubSocket> sub_sock;
int connected_clients = 0;
};
std::unique_ptr<Context> msgq_context;
std::unique_ptr<BridgeZmqContext> zmq_context;
std::unique_ptr<MSGQContext> msgq_context;
std::unique_ptr<ZMQContext> zmq_context;
std::mutex mutex;
std::condition_variable cv;
std::unique_ptr<MSGQPoller> msgq_poller;
std::map<SubSocket *, BridgeZmqPubSocket *> sub2pub;
std::map<SubSocket *, ZMQPubSocket *> sub2pub;
std::vector<SocketPair> socket_pairs;
};

View File

@@ -137,7 +137,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
{"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h
{"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}},
{"Brightness", {PERSISTENT | BACKUP, INT, "0"}},
@@ -230,6 +229,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
{"ModeldSunny", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AlpamayoDriveFast", {PERSISTENT | BACKUP, BOOL, "0"}},
// mapd
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},

View File

@@ -167,92 +167,6 @@ def managed_proc(cmd: list[str], env: dict[str, str]):
proc.kill()
def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None):
rows = [list(row) for row in tabular_data]
def fmt(val):
if isinstance(val, str):
return val
if isinstance(val, (bool, int)):
return str(val)
try:
return format(val, floatfmt)
except (TypeError, ValueError):
return str(val)
formatted = [[fmt(c) for c in row] for row in rows]
hdrs = [str(h) for h in headers] if headers else None
ncols = max((len(r) for r in formatted), default=0)
if hdrs:
ncols = max(ncols, len(hdrs))
if ncols == 0:
return ""
for r in formatted:
r.extend([""] * (ncols - len(r)))
if hdrs:
hdrs.extend([""] * (ncols - len(hdrs)))
widths = [0] * ncols
if hdrs:
for i in range(ncols):
widths[i] = len(hdrs[i])
for row in formatted:
for i in range(ncols):
widths[i] = max(widths[i], max(len(ln) for ln in row[i].split('\n')))
def _align(s, w):
if stralign == "center":
return s.center(w)
return s.ljust(w)
if tablefmt == "html":
parts = ["<table>"]
if hdrs:
parts.append("<thead>")
parts.append("<tr>" + "".join(f"<th>{h}</th>" for h in hdrs) + "</tr>")
parts.append("</thead>")
parts.append("<tbody>")
for row in formatted:
parts.append("<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>")
parts.append("</tbody>")
parts.append("</table>")
return "\n".join(parts)
if tablefmt == "simple_grid":
def _sep(left, mid, right):
return left + mid.join("\u2500" * (w + 2) for w in widths) + right
top, mid_sep, bot = _sep("\u250c", "\u252c", "\u2510"), _sep("\u251c", "\u253c", "\u2524"), _sep("\u2514", "\u2534", "\u2518")
def _fmt_row(cells):
split = [c.split('\n') for c in cells]
nlines = max(len(s) for s in split)
for s in split:
s.extend([""] * (nlines - len(s)))
return ["\u2502" + "\u2502".join(f" {_align(split[i][li], widths[i])} " for i in range(ncols)) + "\u2502" for li in range(nlines)]
lines = [top]
if hdrs:
lines.extend(_fmt_row(hdrs))
lines.append(mid_sep)
for ri, row in enumerate(formatted):
lines.extend(_fmt_row(row))
lines.append(mid_sep if ri < len(formatted) - 1 else bot)
return "\n".join(lines)
# simple
gap = " "
lines = []
if hdrs:
lines.append(gap.join(h.ljust(w) for h, w in zip(hdrs, widths, strict=True)))
lines.append(gap.join("-" * w for w in widths))
for row in formatted:
lines.append(gap.join(_align(row[i], widths[i]) for i in range(ncols)))
return "\n".join(lines)
def retry(attempts=3, delay=1.0, ignore_failure=False):
def decorator(func):
@functools.wraps(func)

2
panda

Submodule panda updated: a95e060e85...ed8a6f9ec2

View File

@@ -89,6 +89,7 @@ testing = [
"pytest-timeout",
"pytest-asyncio",
"pytest-mock",
"pytest-repeat",
"ruff",
"codespell",
"pre-commit-hooks",
@@ -96,12 +97,15 @@ testing = [
dev = [
"av",
"azure-identity",
"azure-storage-blob",
"dictdiffer",
"matplotlib",
"opencv-python-headless",
"parameterized >=0.8, <0.9",
"pyautogui",
"pywinctl",
"tabulate",
]
tools = [

View File

@@ -11,7 +11,7 @@ LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
GLYPH_PADDING = 6
EXTRA_CHARS = "×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ×°§•€£¥"
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
def _languages():

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -22,8 +22,7 @@ LONG_MPC_DIR = os.path.dirname(os.path.abspath(__file__))
EXPORT_DIR = os.path.join(LONG_MPC_DIR, "c_generated_code")
JSON_FILE = os.path.join(LONG_MPC_DIR, "acados_ocp_long.json")
LongitudinalPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
MPC_SOURCES = (LongitudinalPlanSource.lead0, LongitudinalPlanSource.lead1, LongitudinalPlanSource.cruise)
SOURCES = ['lead0', 'lead1', 'cruise', 'e2e']
X_DIM = 3
U_DIM = 1
@@ -108,10 +107,10 @@ def gen_long_model():
a_min = SX.sym('a_min')
a_max = SX.sym('a_max')
x_obstacle = SX.sym('x_obstacle')
a_prev = SX.sym('a_prev')
prev_a = SX.sym('prev_a')
lead_t_follow = SX.sym('lead_t_follow')
lead_danger_factor = SX.sym('lead_danger_factor')
model.p = vertcat(a_min, a_max, x_obstacle, a_prev, lead_t_follow, lead_danger_factor)
model.p = vertcat(a_min, a_max, x_obstacle, prev_a, lead_t_follow, lead_danger_factor)
# dynamics model
f_expl = vertcat(v_ego, a_ego, j_ego)
@@ -143,7 +142,7 @@ def gen_long_ocp():
a_min, a_max = ocp.model.p[0], ocp.model.p[1]
x_obstacle = ocp.model.p[2]
a_prev = ocp.model.p[3]
prev_a = ocp.model.p[3]
lead_t_follow = ocp.model.p[4]
lead_danger_factor = ocp.model.p[5]
@@ -160,7 +159,7 @@ def gen_long_ocp():
x_ego,
v_ego,
a_ego,
a_ego - a_prev,
a_ego - prev_a,
j_ego]
ocp.model.cost_y_expr = vertcat(*costs)
ocp.model.cost_y_expr_e = vertcat(*costs[:-1])
@@ -218,7 +217,7 @@ class LongitudinalMpc:
self.dt = dt
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.reset()
self.source = LongitudinalPlanSource.cruise
self.source = SOURCES[2]
def reset(self):
self.solver.reset()
@@ -228,7 +227,7 @@ class LongitudinalMpc:
self.v_solution = np.zeros(N+1)
self.a_solution = np.zeros(N+1)
self.j_solution = np.zeros(N)
self.a_prev = np.array(self.a_solution)
self.prev_a = np.array(self.a_solution)
self.yref = np.zeros((N+1, COST_DIM))
for i in range(N):
@@ -336,7 +335,7 @@ class LongitudinalMpc:
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
self.source = MPC_SOURCES[np.argmin(x_obstacles[0])]
self.source = SOURCES[np.argmin(x_obstacles[0])]
self.yref[:,:] = 0.0
for i in range(N):
@@ -346,7 +345,7 @@ class LongitudinalMpc:
self.params[:,0] = ACCEL_MIN
self.params[:,1] = ACCEL_MAX
self.params[:,2] = np.min(x_obstacles, axis=1)
self.params[:,3] = np.copy(self.a_prev)
self.params[:,3] = np.copy(self.prev_a)
self.params[:,4] = t_follow
self.params[:,5] = LEAD_DANGER_FACTOR
@@ -378,7 +377,7 @@ class LongitudinalMpc:
self.a_solution = self.x_sol[:,2]
self.j_solution = self.u_sol[:,0]
self.a_prev = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
self.prev_a = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
t = time.monotonic()
if self.solution_status != 0:

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, LongitudinalPlanSource
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
@@ -164,7 +164,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
output_a_target = min(output_a_target_e2e, output_a_target_mpc)
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
if output_a_target < output_a_target_mpc:
self.mpc.source = LongitudinalPlanSource.e2e
self.mpc.source = SOURCES[3]
else:
output_a_target = output_a_target_mpc
self.output_should_stop = output_should_stop_mpc

View File

@@ -1,238 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
from collections import defaultdict
import numpy as np
from tabulate import tabulate
from openpilot.tools.lib.logreader import LogReader
DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19"
MB = 1024 * 1024
TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center")
def _get_procs():
from openpilot.selfdrive.test.test_onroad import PROCS
return PROCS
def is_openpilot_proc(name):
if any(p in name for p in _get_procs()):
return True
# catch openpilot processes not in PROCS (athenad, manager, etc.)
return 'openpilot' in name or name.startswith(('selfdrive.', 'system.'))
def get_proc_name(proc):
if len(proc.cmdline) > 0:
return list(proc.cmdline)[0]
return proc.name
def pct(val_mb, total_mb):
return val_mb / total_mb * 100 if total_mb else 0
def has_pss(proc_logs):
"""Check if logs contain PSS data (new field, not in old logs)."""
try:
for proc in proc_logs[-1].procLog.procs:
if proc.memPss > 0:
return True
except AttributeError:
pass
return False
def print_summary(proc_logs, device_states):
mem = proc_logs[-1].procLog.mem
total = mem.total / MB
used = (mem.total - mem.available) / MB
cached = mem.cached / MB
shared = mem.shared / MB
buffers = mem.buffers / MB
lines = [
f" Total: {total:.0f} MB",
f" Used (total-avail): {used:.0f} MB ({pct(used, total):.0f}%)",
f" Cached: {cached:.0f} MB ({pct(cached, total):.0f}%) Buffers: {buffers:.0f} MB ({pct(buffers, total):.0f}%)",
f" Shared/MSGQ: {shared:.0f} MB ({pct(shared, total):.0f}%)",
]
if device_states:
mem_pcts = [m.deviceState.memoryUsagePercent for m in device_states]
lines.append(f" deviceState memory: {np.min(mem_pcts)}-{np.max(mem_pcts)}% (avg {np.mean(mem_pcts):.0f}%)")
print("\n-- Memory Summary --")
print("\n".join(lines))
return total
def collect_per_process_mem(proc_logs, use_pss):
"""Collect per-process memory samples. Returns {name: {metric: [values_per_sample_in_MB]}}."""
by_proc = defaultdict(lambda: defaultdict(list))
for msg in proc_logs:
sample = defaultdict(lambda: defaultdict(float))
for proc in msg.procLog.procs:
name = get_proc_name(proc)
sample[name]['rss'] += proc.memRss / MB
if use_pss:
sample[name]['pss'] += proc.memPss / MB
sample[name]['pss_anon'] += proc.memPssAnon / MB
sample[name]['pss_shmem'] += proc.memPssShmem / MB
for name, metrics in sample.items():
for metric, val in metrics.items():
by_proc[name][metric].append(val)
return by_proc
def _has_pss_detail(by_proc) -> bool:
"""Check if any process has non-zero pss_anon/pss_shmem (unavailable on some kernels)."""
return any(sum(v.get('pss_anon', [])) > 0 or sum(v.get('pss_shmem', [])) > 0 for v in by_proc.values())
def process_table_rows(by_proc, total_mb, use_pss, show_detail):
"""Build table rows. Returns (rows, total_row)."""
mem_key = 'pss' if use_pss else 'rss'
rows = []
for name in sorted(by_proc, key=lambda n: np.mean(by_proc[n][mem_key]), reverse=True):
m = by_proc[name]
vals = m[mem_key]
avg = round(np.mean(vals))
row = [name, f"{avg} MB", f"{round(np.max(vals))} MB", f"{round(pct(avg, total_mb), 1)}%"]
if show_detail:
row.append(f"{round(np.mean(m['pss_anon']))} MB")
row.append(f"{round(np.mean(m['pss_shmem']))} MB")
rows.append(row)
# Total row
total_row = None
if by_proc:
max_samples = max(len(v[mem_key]) for v in by_proc.values())
totals = []
for i in range(max_samples):
s = sum(v[mem_key][i] for v in by_proc.values() if i < len(v[mem_key]))
totals.append(s)
avg_total = round(np.mean(totals))
total_row = ["TOTAL", f"{avg_total} MB", f"{round(np.max(totals))} MB", f"{round(pct(avg_total, total_mb), 1)}%"]
if show_detail:
total_row.append(f"{round(sum(np.mean(v['pss_anon']) for v in by_proc.values()))} MB")
total_row.append(f"{round(sum(np.mean(v['pss_shmem']) for v in by_proc.values()))} MB")
return rows, total_row
def print_process_tables(op_procs, other_procs, total_mb, use_pss):
all_procs = {**op_procs, **other_procs}
show_detail = use_pss and _has_pss_detail(all_procs)
header = ["process", "avg", "max", "%"]
if show_detail:
header += ["anon", "shmem"]
op_rows, op_total = process_table_rows(op_procs, total_mb, use_pss, show_detail)
# filter other: >5MB avg and not bare interpreter paths (test infra noise)
other_filtered = {n: v for n, v in other_procs.items()
if np.mean(v['pss' if use_pss else 'rss']) > 5.0
and os.path.basename(n.split()[0]) not in ('python', 'python3')}
other_rows, other_total = process_table_rows(other_filtered, total_mb, use_pss, show_detail)
rows = op_rows
if op_total:
rows.append(op_total)
if other_rows:
sep_width = len(header)
rows.append([""] * sep_width)
rows.extend(other_rows)
if other_total:
other_total[0] = "TOTAL (other)"
rows.append(other_total)
metric = "PSS (no shared double-count)" if use_pss else "RSS (includes shared, overcounts)"
print(f"\n-- Per-Process Memory: {metric} --")
print(tabulate(rows, header, **TABULATE_OPTS))
def print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss):
last = proc_logs[-1].procLog.mem
used = (last.total - last.available) / MB
shared = last.shared / MB
cached_buf = (last.buffers + last.cached) / MB - shared # shared (MSGQ) is in Cached; separate it
msgq = shared
mem_key = 'pss' if use_pss else 'rss'
op_total = sum(v[mem_key][-1] for v in op_procs.values()) if op_procs else 0
other_total = sum(v[mem_key][-1] for v in other_procs.values()) if other_procs else 0
proc_sum = op_total + other_total
remainder = used - (cached_buf + msgq) - proc_sum
if not use_pss:
# RSS double-counts shared; add back once to partially correct
remainder += shared
header = ["", "MB", "%", ""]
label = "PSS" if use_pss else "RSS*"
rows = [
["Used (total - avail)", f"{used:.0f}", f"{pct(used, total_mb):.1f}", "memory in use by the system"],
[" Cached + Buffers", f"{cached_buf:.0f}", f"{pct(cached_buf, total_mb):.1f}", "pagecache + fs metadata, reclaimable"],
[" MSGQ (shared)", f"{msgq:.0f}", f"{pct(msgq, total_mb):.1f}", "/dev/shm tmpfs, also in process PSS"],
[f" openpilot {label}", f"{op_total:.0f}", f"{pct(op_total, total_mb):.1f}", "sum of openpilot process memory"],
[f" other {label}", f"{other_total:.0f}", f"{pct(other_total, total_mb):.1f}", "sum of non-openpilot process memory"],
[" kernel/ION/GPU", f"{remainder:.0f}", f"{pct(remainder, total_mb):.1f}", "slab, ION/DMA-BUF, GPU, page tables"],
]
note = "" if use_pss else " (*RSS overcounts shared mem)"
print(f"\n-- Memory Accounting (last sample){note} --")
print(tabulate(rows, header, tablefmt="simple_grid", stralign="right"))
def print_report(proc_logs, device_states=None):
"""Print full memory analysis report. Can be called from tests or CLI."""
if not proc_logs:
print("No procLog messages found")
return
print(f"{len(proc_logs)} procLog samples, {len(device_states or [])} deviceState samples")
use_pss = has_pss(proc_logs)
if not use_pss:
print(" (no PSS data — re-record with updated proclogd for accurate numbers)")
total_mb = print_summary(proc_logs, device_states or [])
by_proc = collect_per_process_mem(proc_logs, use_pss)
op_procs = {n: v for n, v in by_proc.items() if is_openpilot_proc(n)}
other_procs = {n: v for n, v in by_proc.items() if not is_openpilot_proc(n)}
print_process_tables(op_procs, other_procs, total_mb, use_pss)
print_memory_accounting(proc_logs, op_procs, other_procs, total_mb, use_pss)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Analyze memory usage from route logs")
parser.add_argument("route", nargs="?", default=None, help="route ID or local rlog path")
parser.add_argument("--demo", action="store_true", help=f"use demo route ({DEMO_ROUTE})")
args = parser.parse_args()
if args.demo:
route = DEMO_ROUTE
elif args.route:
route = args.route
else:
parser.error("provide a route or use --demo")
print(f"Reading logs from: {route}")
proc_logs = []
device_states = []
for msg in LogReader(route):
if msg.which() == 'procLog':
proc_logs.append(msg)
elif msg.which() == 'deviceState':
device_states.append(msg)
print_report(proc_logs, device_states)

View File

@@ -3,14 +3,15 @@ import time
from cereal import car, log, messaging
from openpilot.common.params import Params
from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model
from openpilot.system.manager.process_config import managed_processes, is_snpe_model, is_tinygrad_model, is_stock_model, is_modeld_sunny
from openpilot.system.hardware import HARDWARE
if __name__ == "__main__":
CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10)
params = Params()
params.put("CarParams", CP.to_bytes())
if is_modeld_sunny:= is_modeld_sunny(False, params, CP):
print("Using sunnypilot custom modeld")
if use_snpe_modeld := is_snpe_model(False, params, CP):
print("Using SNPE modeld")
if use_tinygrad_modeld := is_tinygrad_model(False, params, CP):

View File

@@ -270,7 +270,7 @@ def main():
estimator = LocationEstimator(DEBUG)
filter_initialized = False
critcal_services = ["accelerometer", "gyroscope", "cameraOdometry"]
critcal_services = ["accelerometer", "gyroscope"]
observation_input_invalid = defaultdict(int)
input_invalid_limit = {s: round(INPUT_INVALID_LIMIT * (SERVICE_LIST[s].frequency / 20.)) for s in critcal_services}
@@ -320,8 +320,7 @@ def main():
filter_initialized = sm.all_checks() and sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
if sm.updated["cameraOdometry"]:
critical_service_inputs_valid = all(observation_input_invalid[s] < input_invalid_threshold[s] for s in critcal_services)
inputs_valid = sm.all_valid() and critical_service_inputs_valid
inputs_valid = True
sensors_valid = sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION)
msg = estimator.get_msg(sensors_valid, inputs_valid, filter_initialized)

View File

@@ -50,7 +50,7 @@ def tg_compile(flags, model_name):
# Compile small models
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
flags = {
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0',
'larch64': 'DEV=QCOM',
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
tg_compile(flags, model_name)

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).numpy().flatten()
output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy()
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).numpy().flatten()
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}

View File

@@ -35,14 +35,7 @@ class DRIVER_MONITOR_SETTINGS:
self._EYE_THRESHOLD = 0.65
self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.865
self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4
self._PHONE_THRESH2 = 15.0
self._PHONE_MAX_OFFSET = 0.06
self._PHONE_MIN_OFFSET = 0.025
self._PHONE_DATA_AVG = 0.05
self._PHONE_DATA_VAR = 3*0.005
self._PHONE_MAX_COUNT = int(360 / self._DT_DMON)
self._PHONE_THRESH = 0.5
self._POSE_PITCH_THRESHOLD = 0.3133
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
@@ -152,11 +145,10 @@ class DriverMonitoring:
# init driver status
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2)
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT)
self.pose = DriverPose(settings=self.settings)
self.blink = DriverBlink()
self.phone_prob = 0.
self.always_on = always_on
self.distracted_types = []
@@ -257,12 +249,7 @@ class DriverMonitoring:
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
distracted_types.append(DistractedType.DISTRACTED_BLINK)
if self.phone.prob_calibrated:
using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \
* self.settings._PHONE_THRESH2
else:
using_phone = self.phone.prob > self.settings._PHONE_THRESH
if using_phone:
if self.phone_prob > self.settings._PHONE_THRESH:
distracted_types.append(DistractedType.DISTRACTED_PHONE)
return distracted_types
@@ -301,7 +288,7 @@ class DriverMonitoring:
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.phone.prob = driver_data.phoneProb
self.phone_prob = driver_data.phoneProb
self.distracted_types = self._get_distracted_types()
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
@@ -315,11 +302,9 @@ class DriverMonitoring:
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
self.phone.prob_offseter.push_and_update(self.phone.prob)
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
if self.face_detected and not self.driver_distracted:
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
@@ -425,8 +410,6 @@ class DriverMonitoring:
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
"phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(),
"phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n,
"stepChange": self.step_change,
"awarenessActive": self.awareness_active,
"awarenessPassive": self.awareness_passive,

View File

@@ -6,16 +6,16 @@ import time
import signal
import subprocess
from panda import Panda, PandaDFU, PandaProtocolMismatch, McuType, FW_PATH
from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
from openpilot.common.swaglog import cloudlog
def get_expected_signature() -> bytes:
def get_expected_signature(panda: Panda) -> bytes:
try:
fn = os.path.join(FW_PATH, McuType.H7.config.app_fn)
fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn)
return Panda.get_signature_from_firmware(fn)
except Exception:
cloudlog.exception("Error computing expected signature")
@@ -35,7 +35,7 @@ def flash_panda(panda_serial: str) -> Panda:
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
return panda
fw_signature = get_expected_signature()
fw_signature = get_expected_signature(panda)
internal_panda = panda.is_internal()
panda_version = "bootstub" if panda.bootstub else panda.get_version()

View File

@@ -304,6 +304,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
},
EventName.stockLkas: {
ET.PERMANENT: Alert(
"Stock LKAS: Lane Departure Detected",
"",
AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.),
ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"),
},

View File

@@ -91,7 +91,7 @@ class SelfdriveD(CruiseHelper):
ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP']
if SIMULATION:
ignore += ['driverCameraState', 'managerState']
ignore += ['driverCameraState', 'managerState', 'livePose', 'liveCalibration', 'liveParameters', 'liveTorqueParameters', 'liveDelay', 'driverAssistance']
if REPLAY:
# no vipc in replay will make them ignored anyways
ignore += ['roadCameraState', 'wideRoadCameraState']
@@ -388,8 +388,8 @@ class SelfdriveD(CruiseHelper):
if not self.CP.notCar:
if not self.sm['livePose'].posenetOK:
self.events.add(EventName.posenetInvalid)
if not self.sm['livePose'].inputsOK:
self.events.add(EventName.locationdTemporaryError)
# if not self.sm['livePose'].inputsOK:
# self.events.add(EventName.locationdTemporaryError)
if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
self.events.add(EventName.paramsdTemporaryError)

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]
[--blacklist-cars CARS] [--ignore-fields FIELDS] [--ignore-msgs MSGS] [--update-refs] [--upload-only]
Regression test to identify changes in a process's output
optional arguments:
-h, --help show this help message and exit
@@ -33,6 +33,7 @@ optional arguments:
--ignore-fields IGNORE_FIELDS Extra fields or msgs to ignore (e.g. driverMonitoringState.events)
--ignore-msgs IGNORE_MSGS Msgs to ignore (e.g. onroadEvents)
--update-refs Updates reference logs using current commit
--upload-only Skips testing processes and uploads logs from previous test run
```
## Forks

View File

@@ -9,7 +9,7 @@ from itertools import zip_longest
import matplotlib.pyplot as plt
import numpy as np
from openpilot.common.utils import tabulate
from tabulate import tabulate
from openpilot.common.git import get_commit
from openpilot.system.hardware import PC

View File

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

View File

@@ -9,13 +9,14 @@ from typing import Any
from opendbc.car.car_helpers import interface_names
from openpilot.common.git import get_commit
from openpilot.tools.lib.openpilotci import get_url
from openpilot.tools.lib.openpilotci import get_url, upload_file
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, replay_process, \
check_most_messages_valid
from openpilot.tools.lib.filereader import FileReader
from openpilot.tools.lib.logreader import LogReader, save_log
from openpilot.tools.lib.url_file import URLFile
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
source_segments = [
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
@@ -65,17 +66,46 @@ segments = [
# dashcamOnly makes don't need to be tested until a full port is done
excluded_interfaces = ["mock", "body", "psa"]
BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/"
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
def preserve_only_specified_files_from_ref_commit(*commits_to_keep):
"""Keep only files in fakedata that contain any of the specified commit hashes."""
removed = 0
for f in os.listdir(FAKEDATA):
if not any(commit in f for commit in commits_to_keep):
os.remove(os.path.join(FAKEDATA, f))
removed += 1
if removed > 0:
print(f"Removed {removed} old files from {FAKEDATA}")
def handle_output_file(cur_log_fn, local):
"""Handle the output file based on whether we're using remote or local storage."""
assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}"
if local:
os.system(f"git add '{os.path.realpath(cur_log_fn)}'")
else:
upload_file(cur_log_fn, os.path.basename(cur_log_fn))
os.remove(cur_log_fn)
def run_test_process(data):
segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data
lr = LogReader.from_bytes(lr_dat)
res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
# save logs so we can update refs
save_log(cur_log_fn, log_msgs)
res = None
if not args.upload_only:
lr = LogReader.from_bytes(lr_dat)
res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
# save logs so we can upload when updating refs
save_log(cur_log_fn, log_msgs)
if args.update_refs or args.upload_only:
print(f'Processing: {os.path.basename(cur_log_fn)}')
handle_output_file(cur_log_fn, args.local)
return (segment, cfg.proc_name, res)
@@ -114,6 +144,27 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non
return str(e), log_msgs
def finalize_git_updates(cur_commit, ref_commit_fn):
"""Finalize git updates and create commit."""
try:
# Add all new files first
os.system(f"git add {os.path.realpath(ref_commit_fn)}")
os.system(f"git add {os.path.realpath(FAKEDATA)}/*.zst")
# Clean up old files - keep only new ref files since they're becoming the reference
preserve_only_specified_files_from_ref_commit(cur_commit)
# Add the deletions to git
os.system(f"git add -u {os.path.realpath(FAKEDATA)}")
# Create the commit
commit_msg = f"test_processes: update ref logs to {cur_commit[:7]}"
os.system(f'git commit -m "{commit_msg}"')
print("Successfully committed reference log updates")
except Exception as e:
print(f"Failed to commit changes: {e}")
if __name__ == "__main__":
all_cars = {car for car, _ in segments}
all_procs = {cfg.proc_name for cfg in CONFIGS if cfg.proc_name not in EXCLUDED_PROCS}
@@ -135,6 +186,10 @@ if __name__ == "__main__":
help="Msgs to ignore (e.g. carEvents)")
parser.add_argument("--update-refs", action="store_true",
help="Updates reference logs using current commit")
parser.add_argument("--upload-only", action="store_true",
help="Skips testing processes and uploads logs from previous test run")
parser.add_argument("--local", action="store_true",
help="Use local git/ storage instead of remote (Azure for Comma)")
parser.add_argument("-j", "--jobs", type=int, default=max(cpu_count - 2, 1),
help="Max amount of parallel jobs")
args = parser.parse_args()
@@ -144,21 +199,33 @@ if __name__ == "__main__":
tested_cars = {c.upper() for c in tested_cars}
full_test = (tested_procs == all_procs) and (tested_cars == all_cars) and all(len(x) == 0 for x in (args.ignore_fields, args.ignore_msgs))
upload = args.update_refs or args.upload_only
os.makedirs(os.path.dirname(FAKEDATA), exist_ok=True)
if args.update_refs:
if upload:
assert full_test, "Need to run full test when updating refs"
try:
with open(REF_COMMIT_FN) as f:
ref_commit = f.read().strip()
except FileNotFoundError:
ref_commit = URLFile(BASE_URL + "ref_commit", cache=False).read().decode().strip()
print("Couldn't find reference commit")
sys.exit(1)
cur_commit = get_commit()
if not cur_commit:
raise Exception("Couldn't get current commit")
# Could be set as default in args, but wanted to be more explicit on the flow.
if upload and not args.local and not IS_AZURE_TOKEN_DEFINED:
print("***** Warning: local/git run was used by default since AZURE_TOKEN was NOT found on the env variables! *****")
args.local = True
# Clean up old files before starting
if upload and args.local:
print("***** Cleaning up old fakedata for local/git tracked refs *****")
preserve_only_specified_files_from_ref_commit(cur_commit, ref_commit)
print(f"***** testing against commit {ref_commit} *****")
# check to make sure all car brands are tested
@@ -168,11 +235,12 @@ if __name__ == "__main__":
log_paths: defaultdict[str, dict[str, dict[str, str]]] = defaultdict(lambda: defaultdict(dict))
with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool:
download_segments = [seg for car, seg in segments if car in tested_cars]
log_data: dict[str, LogReader] = {}
p1 = pool.map(get_log_data, download_segments)
for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)):
log_data[segment] = lr
if not args.upload_only:
download_segments = [seg for car, seg in segments if car in tested_cars]
log_data: dict[str, LogReader] = {}
p1 = pool.map(get_log_data, download_segments)
for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)):
log_data[segment] = lr
pool_args: Any = []
for car_brand, segment in segments:
@@ -187,15 +255,15 @@ if __name__ == "__main__":
if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'):
continue
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst".replace("|", "_"))
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
if args.update_refs: # reference logs will not exist if routes were just regenerated
route, seg_num = segment.rsplit("--", 1)
ref_log_path = get_url(route, seg_num, "rlog.zst")
ref_log_path = get_url(*segment.rsplit("--", 1,), "rlog.zst")
else:
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst".replace("|", "_"))
ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.zst")
ref_log_path = ref_log_fn if os.path.exists(ref_log_fn) else BASE_URL + os.path.basename(ref_log_fn)
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, log_data[segment]))
dat = None if args.upload_only else log_data[segment]
pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, dat))
log_paths[segment][cfg.proc_name]['ref'] = ref_log_path
log_paths[segment][cfg.proc_name]['new'] = cur_log_fn
@@ -203,16 +271,19 @@ if __name__ == "__main__":
results: Any = defaultdict(dict)
p2 = pool.map(run_test_process, pool_args)
for (segment, proc, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)):
results[segment][proc] = result
if not args.upload_only:
results[segment][proc] = result
diff_short, diff_long, failed = format_diff(results, log_paths, ref_commit)
if not args.update_refs:
if not upload:
with open(os.path.join(PROC_REPLAY_DIR, "diff.txt"), "w") as f:
f.write(diff_long)
print(diff_short)
if failed:
print("TEST FAILED")
print("\n\nTo push the new reference logs for this commit run:")
print("./test_processes.py --upload-only")
else:
print("TEST SUCCEEDED")
@@ -221,4 +292,8 @@ if __name__ == "__main__":
f.write(cur_commit)
print(f"\n\nUpdated reference logs for commit: {cur_commit}")
# Only do git operations if we're in local mode
if upload and args.local:
finalize_git_updates(cur_commit, REF_COMMIT_FN)
sys.exit(int(failed))

View File

@@ -8,7 +8,7 @@ import time
import numpy as np
from collections import Counter, defaultdict
from pathlib import Path
from openpilot.common.utils import tabulate
from tabulate import tabulate
from cereal import log
import cereal.messaging as messaging
@@ -56,7 +56,7 @@ PROCS = {
"selfdrive.ui.soundd": 3.0,
"selfdrive.ui.feedback.feedbackd": 1.0,
"selfdrive.monitoring.dmonitoringd": 4.0,
"system.proclogd": 7.0,
"system.proclogd": 3.0,
"system.logmessaged": 1.0,
"system.tombstoned": 0,
"system.journald": 1.0,
@@ -282,12 +282,9 @@ class TestOnroad:
print("\n------------------------------------------------")
print("--------------- Memory Usage -------------------")
print("------------------------------------------------")
from openpilot.selfdrive.debug.mem_usage import print_report
print_report(self.msgs['procLog'], self.msgs['deviceState'])
offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET)
mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]]
print("Overall memory usage: ", mems)
print("MSGQ (/dev/shm/) usage: ", subprocess.check_output(["du", "-hs", "/dev/shm"]).split()[0].decode())
# check for big leaks. note that memory usage is

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, scroll_indicator=False)
], spacing=0, pad_start=0, pad_end=0)
self._scroller.set_reset_scroll_at_show(False)
# Disable scrolling when onroad is interacting with bookmark

View File

@@ -1,5 +1,6 @@
import os
import threading
import json
import pyray as rl
from enum import IntEnum
from collections.abc import Callable
@@ -10,7 +11,7 @@ from openpilot.common.time_helpers import system_time_valid
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide
@@ -120,9 +121,6 @@ class PairBigButton(BigButton):
def __init__(self):
super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png", icon_size=(33, 60))
def _get_label_font_size(self):
return 64
def _update_state(self):
if ui_state.prime_state.is_paired():
self.set_text("paired")
@@ -224,7 +222,7 @@ class UpdateOpenpilotBigButton(BigButton):
if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT:
self.set_rotate_icon(False)
self.set_value("updater failed\nto respond")
self.set_value("updater failed to respond")
self._state = UpdaterState.IDLE
self._hide_value_t = rl.get_time()
@@ -305,14 +303,30 @@ class DeviceLayoutMici(NavWidget):
self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66))
self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off"))
self._load_languages()
def language_callback():
def selected_language_callback():
selected_language = dlg.get_selected_option()
ui_state.params.put("LanguageSetting", self._languages[selected_language])
current_language_name = ui_state.params.get("LanguageSetting")
current_language = next(name for name, lang in self._languages.items() if lang == current_language_name)
dlg = BigMultiOptionDialog(list(self._languages), default=current_language, right_btn_callback=selected_language_callback)
gui_app.set_modal_overlay(dlg)
# lang_button = BigButton("change language", "", "icons_mici/settings/device/language.png")
# lang_button.set_click_callback(language_callback)
regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png")
regulatory_btn.set_click_callback(self._on_regulatory)
driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png")
driver_cam_btn = BigButton("driver camera preview", "", "icons_mici/settings/device/cameras.png")
driver_cam_btn.set_click_callback(self._show_driver_camera)
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png")
review_training_guide_btn = BigButton("review training guide", "", "icons_mici/settings/device/info.png")
review_training_guide_btn.set_click_callback(self._on_review_training_guide)
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
@@ -339,7 +353,7 @@ class DeviceLayoutMici(NavWidget):
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog)
gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None))
def _offroad_transition(self):
self._power_off_btn.set_visible(ui_state.is_offroad())
@@ -357,6 +371,10 @@ class DeviceLayoutMici(NavWidget):
self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None))
def _load_languages(self):
with open(os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")) as f:
self._languages = json.load(f)
def show_event(self):
super().show_event()
self._scroller.show_event()

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, WifiIcon, normalize_ssid
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigCircleToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
@@ -39,7 +39,8 @@ class NetworkLayoutMici(NavWidget):
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
self._tethering_toggle_btn = BigCircleToggle("icons_mici/tethering_short.png", toggle_callback=tethering_toggle_callback,
icon_size=(82, 82), icon_offset=(0, 12))
def tethering_password_callback(password: str):
if password:
@@ -55,6 +56,9 @@ class NetworkLayoutMici(NavWidget):
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** IP Address ********
self._ip_address_btn = BigButton("IP Address", "Not connected")
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
@@ -70,13 +74,8 @@ class NetworkLayoutMici(NavWidget):
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56)
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47)
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True)
self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
wifi_button = BigButton("wi-fi")
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
# ******** Advanced settings ********
# ******** Roaming toggle ********
@@ -91,7 +90,7 @@ class NetworkLayoutMici(NavWidget):
# Main scroller ----------------------------------
self._scroller = Scroller([
self._wifi_button,
wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
@@ -100,6 +99,7 @@ class NetworkLayoutMici(NavWidget):
self._apn_btn,
self._cellular_metered_btn,
# */
self._ip_address_btn,
], snap_items=False)
# Set initial config
@@ -158,22 +158,8 @@ class NetworkLayoutMici(NavWidget):
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update wi-fi button with ssid and ip address
# TODO: make sure we handle hidden ssids
connected_network = next((network for network in networks if network.is_connected), None)
self._wifi_button.set_text(normalize_ssid(connected_network.ssid) if connected_network is not None else "wi-fi")
self._wifi_button.set_value(self._wifi_manager.ipv4_address or "not connected")
if connected_network is not None:
strength = WifiIcon.get_strength_icon_idx(connected_network.strength)
if strength == 2:
strength_icon = self._wifi_full_txt
elif strength == 1:
strength_icon = self._wifi_medium_txt
else:
strength_icon = self._wifi_low_txt
self._wifi_button.set_icon(strength_icon)
else:
self._wifi_button.set_icon(self._wifi_slash_txt)
# Update IP address
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
# Update network metered
self._network_metered_btn.set_value(

View File

@@ -50,16 +50,12 @@ class WifiIcon(Widget):
def set_scale(self, scale: float):
self._scale = scale
@staticmethod
def get_strength_icon_idx(strength: int) -> int:
return round(strength / 100 * 2)
def _render(self, _):
if self._network is None:
return
# Determine which wifi strength icon to use
strength = self.get_strength_icon_idx(self._network.strength)
strength = round(self._network.strength / 100 * 2)
if strength == 2:
strength_icon = self._wifi_full_txt
elif strength == 1:
@@ -318,7 +314,7 @@ class WifiUIMici(BigMultiOptionDialog):
INACTIVITY_TIMEOUT = 1
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
super().__init__([], None)
super().__init__([], None, None, right_btn_callback=None)
# Set up back navigation
self.set_back_callback(back_callback)

View File

@@ -30,27 +30,22 @@ class PanelInfo:
instance: Widget
class SettingsBigButton(BigButton):
def _get_label_font_size(self):
return 64
class SettingsLayout(NavWidget):
def __init__(self):
super().__init__()
self._params = Params()
self._current_panel = None # PanelType.DEVICE
toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png")
toggles_btn = BigButton("toggles", "", "icons_mici/settings.png")
toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES))
network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK))
device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE))
developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER))
firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
firehose_btn = BigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE))
self._scroller = Scroller([

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, FirstOrderFilter
from openpilot.common.filter_simple import BounceFilter
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
from openpilot.common.transformations.orientation import rot_from_euler
from enum import IntEnum
@@ -169,7 +169,6 @@ class AugmentedRoadView(CameraView):
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
# debug
self._pm = messaging.PubMaster(['uiDebug'])
@@ -222,11 +221,8 @@ class AugmentedRoadView(CameraView):
# Draw all UI overlays
self._model_renderer.render(self._content_rect)
# Fade out bottom of overlays for looks (only when engaged)
fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
if fade_alpha > 1e-2:
rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0,
rl.Color(255, 255, 255, int(255 * fade_alpha)))
# Fade out bottom of overlays for looks
rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE)
alert_to_render, not_animating_out = self._alert_renderer.will_render()

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, ModelRendererSP
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
@@ -51,10 +51,9 @@ class LeadVehicle:
fill_alpha: int = 0
class ModelRenderer(Widget, ModelRendererSP):
class ModelRenderer(Widget):
def __init__(self):
Widget.__init__(self)
ModelRendererSP.__init__(self)
super().__init__()
self._longitudinal_control = False
self._experimental_mode = False
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
@@ -341,10 +340,6 @@ class ModelRenderer(Widget, ModelRendererSP):
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
self._blend_filter.update(int(allow_throttle))
if ui_state.rainbow_path:
self.rainbow_path.draw_rainbow_path(self._rect, self._path)
return
if self._experimental_mode:
# Draw with acceleration coloring
if ui_state.status == UIStatus.DISENGAGED:

View File

@@ -3,8 +3,9 @@ from typing import Union
from enum import Enum
from collections.abc import Callable
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.label import MiciLabel
from openpilot.system.ui.widgets.scroller import DO_ZOOM
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.common.filter_simple import BounceFilter
@@ -17,7 +18,6 @@ SCROLLING_SPEED_PX_S = 50
COMPLICATION_SIZE = 36
LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9))
LABEL_HORIZONTAL_PADDING = 40
LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma
COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255)
PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07
@@ -52,12 +52,6 @@ class BigCircleButton(Widget):
def set_enable_pressed_state(self, pressed: bool):
self._press_state_enabled = pressed
def _draw_content(self, btn_y: float):
# draw icon
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0],
btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color)
def _render(self, _):
# draw background
txt_bg = self._txt_btn_bg if not self._red else self._txt_btn_red_bg
@@ -71,7 +65,10 @@ class BigCircleButton(Widget):
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
self._draw_content(btn_y)
# draw icon
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0]),
int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), icon_color)
class BigCircleToggle(BigCircleButton):
@@ -96,41 +93,48 @@ class BigCircleToggle(BigCircleButton):
if self._toggle_callback:
self._toggle_callback(self._checked)
def _draw_content(self, btn_y: float):
super()._draw_content(btn_y)
def _render(self, _):
super()._render(_)
# draw status icon
rl.draw_texture_ex(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2, btn_y + 5),
0, 1.0, rl.WHITE)
rl.draw_texture(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
int(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2),
int(self._rect.y + 5), rl.WHITE)
class BigButton(Widget):
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64),
scroll: bool = False):
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64)):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, 402, 180))
self.text = text
self.value = value
self._icon_size = icon_size
self._scroll = scroll
self.set_icon(icon)
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._rotate_icon_t: float | None = None
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
line_height=0.9)
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._update_label_layout()
self._label_font = gui_app.font(FontWeight.DISPLAY)
self._value_font = gui_app.font(FontWeight.ROMAN)
self._label = MiciLabel(text, font_size=self._get_label_font_size(), width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
font_weight=FontWeight.DISPLAY, color=LABEL_COLOR,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
self._sub_label = MiciLabel(value, font_size=COMPLICATION_SIZE, width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
font_weight=FontWeight.ROMAN, color=COMPLICATION_GREY,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
self._load_images()
# internal state
self._scroll_offset = 0 # in pixels
self._needs_scroll = measure_text_cached(self._label_font, text, self._get_label_font_size()).x + 25 > self._rect.width
self._scroll_timer = 0
self._scroll_state = ScrollState.PRE_SCROLL
def set_icon(self, icon: Union[str, rl.Texture]):
self._txt_icon = gui_app.texture(icon, *self._icon_size) if isinstance(icon, str) and len(icon) else icon
@@ -145,33 +149,28 @@ class BigButton(Widget):
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
self._txt_hover_bg = gui_app.texture("icons_mici/buttons/button_rectangle_hover.png", 402, 180)
def _width_hint(self) -> int:
# Single line if scrolling, so hide behind icon if exists
icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - icon_size)
def _get_label_font_size(self):
if len(self.text) <= 18:
return 48
if len(self.text) < 12:
font_size = 64
elif len(self.text) < 17:
font_size = 48
elif len(self.text) < 20:
font_size = 42
else:
return 42
font_size = 36
def _update_label_layout(self):
self._label.set_font_size(self._get_label_font_size())
if self.value:
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
else:
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
font_size -= 20
return font_size
def set_text(self, text: str):
self.text = text
self._label.set_text(text)
self._update_label_layout()
def set_value(self, value: str):
self.value = value
self._sub_label.set_text(value)
self._update_label_layout()
def get_value(self) -> str:
return self.value
@@ -179,35 +178,37 @@ class BigButton(Widget):
def get_text(self):
return self.text
def _draw_content(self, btn_y: float):
# LABEL ------------------------------------------------------------------
label_x = self._rect.x + LABEL_HORIZONTAL_PADDING
def _update_state(self):
# hold on text for a bit, scroll, hold again, reset
if self._needs_scroll:
"""`dt` should be seconds since last frame (rl.get_frame_time())."""
# TODO: this comment is generated by GPT, prob wrong and misused
dt = rl.get_frame_time()
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
self._label.set_color(label_color)
label_rect = rl.Rectangle(label_x, btn_y + LABEL_VERTICAL_PADDING, self._width_hint(),
self._rect.height - LABEL_VERTICAL_PADDING * 2)
self._label.render(label_rect)
self._scroll_timer += dt
if self._scroll_state == ScrollState.PRE_SCROLL:
if self._scroll_timer < 0.5:
return
self._scroll_state = ScrollState.SCROLLING
self._scroll_timer = 0
if self.value:
label_y = btn_y + self._rect.height - LABEL_VERTICAL_PADDING
sub_label_height = self._sub_label.get_content_height(self._width_hint())
sub_label_rect = rl.Rectangle(label_x, label_y - sub_label_height, self._width_hint(), sub_label_height)
self._sub_label.render(sub_label_rect)
elif self._scroll_state == ScrollState.SCROLLING:
self._scroll_offset -= SCROLLING_SPEED_PX_S * dt
# reset when text has completely left the button + 50 px gap
# TODO: use global constant for 30+30 px gap
# TODO: add std Widget padding option integrated into the self._rect
full_len = measure_text_cached(self._label_font, self.text, self._get_label_font_size()).x + 30 + 30
if self._scroll_offset < (self._rect.width - full_len):
self._scroll_state = ScrollState.POST_SCROLL
self._scroll_timer = 0
# ICON -------------------------------------------------------------------
if self._txt_icon:
rotation = 0
if self._rotate_icon_t is not None:
rotation = (rl.get_time() - self._rotate_icon_t) * 180
# draw top right with 30px padding
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
y = btn_y + 30 + self._txt_icon.height / 2
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height)
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
elif self._scroll_state == ScrollState.POST_SCROLL:
# wait for a bit before starting to scroll again
if self._scroll_timer < 0.75:
return
self._scroll_state = ScrollState.PRE_SCROLL
self._scroll_timer = 0
self._scroll_offset = 0
def _render(self, _):
# draw _txt_default_bg
@@ -222,7 +223,33 @@ class BigButton(Widget):
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
self._draw_content(btn_y)
# LABEL ------------------------------------------------------------------
lx = self._rect.x + LABEL_HORIZONTAL_PADDING
ly = btn_y + self._rect.height - 33 # - 40# - self._get_label_font_size() / 2
if self.value:
self._sub_label.set_position(lx, ly)
ly -= self._sub_label.font_size + 9
self._sub_label.render()
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
self._label.set_color(label_color)
self._label.set_position(lx, ly)
self._label.render()
# ICON -------------------------------------------------------------------
if self._txt_icon:
rotation = 0
if self._rotate_icon_t is not None:
rotation = (rl.get_time() - self._rotate_icon_t) * 180
# drop top right with 30px padding
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
y = self._rect.y + 30 + self._txt_icon.height / 2
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
dest_rec = rl.Rectangle(int(x), int(y), self._txt_icon.width, self._txt_icon.height)
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
class BigToggle(BigButton):
@@ -231,6 +258,8 @@ class BigToggle(BigButton):
self._checked = initial_state
self._toggle_callback = toggle_callback
self._label.set_font_size(48)
def _load_images(self):
super()._load_images()
self._txt_enabled_toggle = gui_app.texture("icons_mici/buttons/toggle_pill_enabled.png", 84, 66)
@@ -248,15 +277,15 @@ class BigToggle(BigButton):
def _draw_pill(self, x: float, y: float, checked: bool):
# draw toggle icon top right
if checked:
rl.draw_texture_ex(self._txt_enabled_toggle, (x, y), 0, 1.0, rl.WHITE)
rl.draw_texture(self._txt_enabled_toggle, int(x), int(y), rl.WHITE)
else:
rl.draw_texture_ex(self._txt_disabled_toggle, (x, y), 0, 1.0, rl.WHITE)
rl.draw_texture(self._txt_disabled_toggle, int(x), int(y), rl.WHITE)
def _draw_content(self, btn_y: float):
super()._draw_content(btn_y)
def _render(self, _):
super()._render(_)
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
y = btn_y
y = self._rect.y
self._draw_pill(x, y, self._checked)
@@ -268,10 +297,15 @@ class BigMultiToggle(BigToggle):
self._options = options
self._select_callback = select_callback
self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width))
# TODO: why isn't this automatic?
self._label.set_font_size(self._get_label_font_size())
self.set_value(self._options[0])
def _width_hint(self) -> int:
return int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)
def _get_label_font_size(self):
font_size = super()._get_label_font_size()
return font_size - 6
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
@@ -281,14 +315,13 @@ class BigMultiToggle(BigToggle):
if self._select_callback:
self._select_callback(self.value)
def _draw_content(self, btn_y: float):
# don't draw pill from BigToggle
BigButton._draw_content(self, btn_y)
def _render(self, _):
BigButton._render(self, _)
checked_idx = self._options.index(self.value)
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
y = btn_y
y = self._rect.y
for i in range(len(self._options)):
self._draw_pill(x, y, checked_idx == i)

View File

@@ -14,6 +14,7 @@ from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
DEBUG = False
@@ -21,17 +22,32 @@ PADDING = 20
class BigDialogBase(NavWidget, abc.ABC):
def __init__(self):
def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None):
super().__init__()
self._ret = DialogResult.NO_ACTION
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL))
self._right_btn = None
if right_btn:
def right_btn_callback_wrapper():
gui_app.set_modal_overlay(None)
if right_btn_callback:
right_btn_callback()
self._right_btn = SideButton(right_btn)
self._right_btn.set_click_callback(right_btn_callback_wrapper)
# move to right side
self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width
def _render(self, _) -> DialogResult:
"""
Allows `gui_app.set_modal_overlay(BigDialog(...))`.
The overlay runner keeps calling until result != NO_ACTION.
"""
if self._right_btn:
self._right_btn.set_position(self._right_btn._rect.x, self._rect.y)
self._right_btn.render()
return self._ret
@@ -39,8 +55,10 @@ class BigDialogBase(NavWidget, abc.ABC):
class BigDialog(BigDialogBase):
def __init__(self,
title: str,
description: str):
super().__init__()
description: str,
right_btn: str | None = None,
right_btn_callback: Callable | None = None):
super().__init__(right_btn, right_btn_callback)
self._title = title
self._description = description
@@ -52,6 +70,8 @@ class BigDialog(BigDialogBase):
# TODO: coming up with these numbers manually is a pain and not scalable
# TODO: no clue what any of these numbers mean. VBox and HBox would remove all of this shite
max_width = self._rect.width - PADDING * 2
if self._right_btn:
max_width -= self._right_btn._rect.width
title_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.BOLD), self._title, 50, int(max_width)))
title_size = measure_text_cached(gui_app.font(FontWeight.BOLD), title_wrapped, 50)
@@ -119,7 +139,7 @@ class BigInputDialog(BigDialogBase):
default_text: str = "",
minimum_length: int = 1,
confirm_callback: Callable[[str], None] | None = None):
super().__init__()
super().__init__(None, None)
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
font_weight=FontWeight.MEDIUM)
self._keyboard = MiciKeyboard()
@@ -131,8 +151,7 @@ class BigInputDialog(BigDialogBase):
self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36)
self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 76, 62)
self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 76, 62)
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 42, 36)
self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
# rects for top buttons
@@ -167,9 +186,9 @@ class BigInputDialog(BigDialogBase):
text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, self.TEXT_INPUT_SIZE)
bg_block_margin = 5
text_x = PADDING / 2 + self._enter_img.width + PADDING
text_x = PADDING * 2 + self._enter_img.width + bg_block_margin
text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin,
int(self._rect.width - text_x * 2),
int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width) - bg_block_margin * 2,
int(text_size.y))
# draw text input
@@ -205,7 +224,7 @@ class BigInputDialog(BigDialogBase):
self._backspace_img_alpha.update(255 * bool(text))
if self._backspace_img_alpha.x > 1:
color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x))
rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(self._rect.y + 14), color)
rl.draw_texture(self._backspace_img, int(self._rect.width - self._enter_img.width - 15), int(text_field_rect.y), color)
if not text and self._hint_label.text and not candidate_char:
# draw description if no text entered yet and not drawing candidate char
@@ -217,12 +236,10 @@ class BigInputDialog(BigDialogBase):
self._top_right_button_rect = rl.Rectangle(text_field_rect.x + text_field_rect.width, self._rect.y,
self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height)
# draw enter button
self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0)
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_disabled_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 255 * 0.35)
if self._enter_img_alpha.x > 1:
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
rl.draw_texture(self._enter_img, int(self._rect.x + 15), int(text_field_rect.y), color)
# keyboard goes over everything
self._keyboard.render(self._rect)
@@ -290,8 +307,9 @@ class BigDialogOptionButton(Widget):
class BigMultiOptionDialog(BigDialogBase):
BACK_TOUCH_AREA_PERCENTAGE = 0.1
def __init__(self, options: list[str], default: str | None):
super().__init__()
def __init__(self, options: list[str], default: str | None,
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] | None = None):
super().__init__(right_btn, right_btn_callback=right_btn_callback)
self._options = options
if default is not None:
assert default in options
@@ -304,6 +322,8 @@ class BigMultiOptionDialog(BigDialogBase):
self._can_click = True
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True)
if self._right_btn is not None:
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
for option in options:
self._scroller.add_widget(BigDialogOptionButton(option))

View File

@@ -0,0 +1,31 @@
import pyray as rl
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.application import gui_app
# ---------------------------------------------------------------------------
# Constants extracted from the original Qt style
# ---------------------------------------------------------------------------
# TODO: this should be corrected, but Scroller relies on this being incorrect :/
WIDTH, HEIGHT = 112, 240
class SideButton(Widget):
def __init__(self, btn_type: str):
super().__init__()
self.type = btn_type
self.set_rect(rl.Rectangle(0, 0, WIDTH, HEIGHT))
# load pre-rendered button images
if btn_type not in ("check", "back"):
btn_type = "back"
btn_img_path = f"icons_mici/buttons/button_side_{btn_type}.png"
btn_img_pressed_path = f"icons_mici/buttons/button_side_{btn_type}_pressed.png"
self._txt_btn, self._txt_btn_back = gui_app.texture(btn_img_path, 100, 224), gui_app.texture(btn_img_pressed_path, 100, 224)
def _render(self, _) -> bool:
x = int(self._rect.x + 12)
y = int(self._rect.y + (self._rect.height - self._txt_btn.height) / 2)
rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back,
x, y, rl.WHITE)
return False

View File

@@ -4,190 +4,27 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from enum import IntEnum
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_settings import SpeedLimitSettingsLayout
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, simple_button_item_sp
from openpilot.system.ui.widgets import Widget
from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
class PanelType(IntEnum):
CRUISE = 0
SLA = 1
ICBM_DESC = tr_noop("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons " +
"by emulating button presses for limited longitudinal control.")
ICMB_UNAVAILABLE = tr_noop("Intelligent Cruise Button Management is currently unavailable on this platform.")
ICMB_UNAVAILABLE_LONG_AVAILABLE = tr_noop("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.")
ICMB_UNAVAILABLE_LONG_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control is the default longitudinal control for this platform.")
ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments for cruise speed increase/decrease.")
ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot longitudinal control enabled.")
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
from openpilot.system.ui.widgets import Widget
class CruiseLayout(Widget):
def __init__(self):
super().__init__()
self._current_panel = PanelType.CRUISE
self._speed_limit_layout = SpeedLimitSettingsLayout(lambda: self._set_current_panel(PanelType.CRUISE))
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
self.icbm_toggle = toggle_item_sp(
title=tr("Intelligent Cruise Button Management (ICBM) (Alpha)"),
description="",
param="IntelligentCruiseButtonManagement")
self.scc_v_toggle = toggle_item_sp(
title=tr("Smart Cruise Control - Vision"),
description=tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."),
param="SmartCruiseControlVision")
self.scc_m_toggle = toggle_item_sp(
title=tr("Smart Cruise Control - Map"),
description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."),
param="SmartCruiseControlMap")
self.custom_acc_toggle = toggle_item_sp(
title=tr("Custom ACC Speed Increments"),
description="",
param="CustomAccIncrementsEnabled",
callback=self._on_custom_acc_toggle)
self.custom_acc_short_increment = option_item_sp(
title=tr("Short Press Increment"),
param="CustomAccShortPressIncrement",
min_value=1, max_value=10, value_change_step=1,
inline=True)
self.custom_acc_long_increment = option_item_sp(
title=tr("Long Press Increment"),
param="CustomAccLongPressIncrement",
value_map={1: 1, 2: 5, 3: 10},
min_value=1, max_value=3, value_change_step=1,
inline=True)
self.sla_settings_button = simple_button_item_sp(
button_text=lambda: tr("Speed Limit"),
button_width=800,
callback=lambda: self._set_current_panel(PanelType.SLA)
)
self.dec_toggle = toggle_item_sp(
title=tr("Enable Dynamic Experimental Control"),
description=tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."),
param="DynamicExperimentalControl")
items = [
self.icbm_toggle,
self.dec_toggle,
self.scc_v_toggle,
self.scc_m_toggle,
self.custom_acc_toggle,
self.custom_acc_short_increment,
self.custom_acc_long_increment,
self.sla_settings_button,
]
return items
def _render(self, rect):
if self._current_panel == PanelType.SLA:
self._speed_limit_layout.render(rect)
else:
self._scroller.render(rect)
self._scroller.render(rect)
def show_event(self):
self._set_current_panel(PanelType.CRUISE)
self._scroller.show_event()
self.icbm_toggle.show_description(True)
self.custom_acc_toggle.show_description(True)
def _set_current_panel(self, panel: PanelType):
self._current_panel = panel
if panel == PanelType.SLA:
self._speed_limit_layout.show_event()
def _update_state(self):
super()._update_state()
if ui_state.CP is not None and ui_state.CP_SP is not None:
has_icbm = ui_state.has_icbm
has_long = ui_state.has_longitudinal_control
if ui_state.CP_SP.intelligentCruiseButtonManagementAvailable and not has_long:
self.icbm_toggle.action_item.set_enabled(ui_state.is_offroad())
self.icbm_toggle.set_description(tr(ICBM_DESC))
else:
ui_state.params.remove("IntelligentCruiseButtonManagement")
self.icbm_toggle.action_item.set_enabled(False)
long_desc = ICMB_UNAVAILABLE
if has_long:
if ui_state.CP.alphaLongitudinalAvailable:
long_desc += " " + ICMB_UNAVAILABLE_LONG_AVAILABLE
else:
long_desc += " " + ICMB_UNAVAILABLE_LONG_UNAVAILABLE
new_desc = "<b>" + tr(long_desc) + "</b>\n\n" + tr(ICBM_DESC)
if self.icbm_toggle.description != new_desc:
self.icbm_toggle.set_description(new_desc)
self.icbm_toggle.show_description(True)
if has_long or has_icbm:
self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad())
self.dec_toggle.action_item.set_enabled(has_long)
self.scc_v_toggle.action_item.set_enabled(True)
self.scc_m_toggle.action_item.set_enabled(True)
else:
ui_state.params.remove("CustomAccIncrementsEnabled")
ui_state.params.remove("DynamicExperimentalControl")
ui_state.params.remove("SmartCruiseControlVision")
ui_state.params.remove("SmartCruiseControlMap")
self.custom_acc_toggle.action_item.set_enabled(False)
self.dec_toggle.action_item.set_enabled(False)
self.scc_v_toggle.action_item.set_enabled(False)
self.scc_m_toggle.action_item.set_enabled(False)
else:
has_icbm = has_long = False
self.icbm_toggle.action_item.set_enabled(False)
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
show_custom_acc_desc = False
if ui_state.is_offroad():
new_custom_acc_desc = tr(ONROAD_ONLY_DESCRIPTION)
show_custom_acc_desc = True
else:
if has_long or has_icbm:
if has_long and ui_state.CP.pcmCruise:
new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION)
show_custom_acc_desc = True
else:
new_custom_acc_desc = tr(ACC_ENABLED_DESCRIPTION)
else:
new_custom_acc_desc = tr(ACC_NOLONG_DESCRIPTION)
show_custom_acc_desc = True
self.custom_acc_toggle.action_item.set_state(False)
if self.custom_acc_toggle.description != new_custom_acc_desc:
self.custom_acc_toggle.set_description(new_custom_acc_desc)
if show_custom_acc_desc:
self.custom_acc_toggle.show_description(True)
self._on_custom_acc_toggle(self.custom_acc_toggle.action_item.get_state())
def _on_custom_acc_toggle(self, state):
self.custom_acc_short_increment.set_visible(state)
self.custom_acc_long_increment.set_visible(state)
self.custom_acc_short_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled)
self.custom_acc_long_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled)

View File

@@ -1,65 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections.abc import Callable
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
from openpilot.system.ui.widgets.network import NavButton
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description
SPEED_LIMIT_POLICY_BUTTONS = [tr("Car Only"), tr("Map Only"), tr("Car First"), tr("Map First"), tr("Combined")]
SPEED_LIMIT_POLICY_DESCRIPTIONS = [
tr("Car Only: Use Speed Limit data only from Car"),
tr("Map Only: Use Speed Limit data only from OpenStreetMaps"),
tr("Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps"),
tr("Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car"),
tr("Combined: Use combined Speed Limit data from Car & OpenStreetMaps")
]
class SpeedLimitPolicyLayout(Widget):
def __init__(self, back_btn_callback: Callable):
super().__init__()
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(back_btn_callback)
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=False, spacing=0)
def _initialize_items(self):
self._speed_limit_policy = multiple_button_item_sp(
title=lambda: tr("Speed Limit Source"),
description=self._get_policy_description,
buttons=SPEED_LIMIT_POLICY_BUTTONS,
param="SpeedLimitPolicy",
button_width=250,
)
items = [
self._speed_limit_policy
]
return items
@staticmethod
def _get_policy_description():
return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS)
def _render(self, rect):
self._back_button.set_position(self._rect.x, self._rect.y + 20)
self._back_button.render()
content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40)
self._scroller.render(content_rect)
def show_event(self):
self._scroller.show_event()
self._speed_limit_policy.show_description(True)

View File

@@ -1,178 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections.abc import Callable
from enum import IntEnum
import pyray as rl
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_policy import SpeedLimitPolicyLayout
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import OffsetType as SpeedLimitOffsetType
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, option_item_sp, simple_button_item_sp, LineSeparatorSP
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import NavButton
from openpilot.system.ui.widgets.scroller_tici import Scroller
SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")]
SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")]
SPEED_LIMIT_MODE_DESCRIPTIONS = [
tr("Off: Disables the Speed Limit functions."),
tr("Information: Displays the current road's speed limit."),
tr("Warning: Provides a warning when exceeding the current road's speed limit."),
tr("Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."),
]
SPEED_LIMIT_OFFSET_DESCRIPTIONS = [
tr("None: No Offset"),
tr("Fixed: Adds a fixed offset [Speed Limit + Offset]"),
tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"),
]
class PanelType(IntEnum):
SETTINGS = 0
POLICY = 1
class SpeedLimitSettingsLayout(Widget):
def __init__(self, back_btn_callback: Callable):
super().__init__()
self._current_panel = PanelType.SETTINGS
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(back_btn_callback)
self._policy_layout = SpeedLimitPolicyLayout(lambda: self._set_current_panel(PanelType.SETTINGS))
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=False, spacing=0)
def _initialize_items(self):
self._speed_limit_mode = multiple_button_item_sp(
title=lambda: tr("Speed Limit"),
description=self._get_mode_description,
buttons=SPEED_LIMIT_MODE_BUTTONS,
param="SpeedLimitMode",
button_width=380,
)
self._source_button = simple_button_item_sp(
button_text=lambda: tr("Customize Source"),
button_width=720,
callback=lambda: self._set_current_panel(PanelType.POLICY)
)
self._speed_limit_offset_type = multiple_button_item_sp(
title=lambda: tr("Speed Limit Offset"),
description="",
buttons=SPEED_LIMIT_OFFSET_TYPE_BUTTONS,
param="SpeedLimitOffsetType",
button_width=450,
)
self._speed_limit_value_offset = option_item_sp(
title="",
param="SpeedLimitValueOffset",
min_value=-30,
max_value=30,
description=self._get_offset_description,
label_callback=self._get_offset_label,
)
items = [
self._speed_limit_mode,
LineSeparatorSP(40),
self._source_button,
LineSeparatorSP(40),
self._speed_limit_offset_type,
self._speed_limit_value_offset
]
return items
def _set_current_panel(self, panel: PanelType):
self._current_panel = panel
if panel == PanelType.POLICY:
self._policy_layout.show_event()
@staticmethod
def _get_mode_description():
return get_highlighted_description(ui_state.params, "SpeedLimitMode", SPEED_LIMIT_MODE_DESCRIPTIONS)
@staticmethod
def _get_offset_description():
return get_highlighted_description(ui_state.params, "SpeedLimitOffsetType", SPEED_LIMIT_OFFSET_DESCRIPTIONS)
@staticmethod
def _get_offset_label(value):
offset_type = int(ui_state.params.get("SpeedLimitOffsetType", return_default=True))
unit = tr("km/h") if ui_state.is_metric else tr("mph")
if offset_type == int(SpeedLimitOffsetType.percentage):
return f"{value}%"
elif offset_type == int(SpeedLimitOffsetType.fixed):
return f"{value} {unit}"
return str(value)
def _update_state(self):
super()._update_state()
speed_limit_mode_param = ui_state.params.get("SpeedLimitMode", return_default=True)
if ui_state.CP is not None and ui_state.CP_SP is not None:
brand = ui_state.CP.brand
has_long = ui_state.has_longitudinal_control
has_icbm = ui_state.has_icbm
"""
Speed Limit Assist is available when:
- has_long or has_icbm, and
- is not a release branch or not a disallowed brand, and
- is not always disallwed
"""
sla_disallow_in_release = brand == "tesla" and ui_state.is_sp_release
sla_always_disallow = brand == "rivian"
sla_available = (has_long or has_icbm) and not sla_disallow_in_release and not sla_always_disallow
if not sla_available and speed_limit_mode_param == int(SpeedLimitMode.assist):
ui_state.params.put("SpeedLimitMode", int(SpeedLimitMode.warning))
else:
sla_available = False
if not sla_available:
self._speed_limit_mode.action_item.set_enabled_buttons({
int(SpeedLimitMode.off),
int(SpeedLimitMode.information),
int(SpeedLimitMode.warning),
})
else:
self._speed_limit_mode.action_item.set_enabled_buttons(None)
offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True)
self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off))
def _render(self, rect):
if self._current_panel == PanelType.POLICY:
self._policy_layout.render(rect)
return
self._back_button.set_position(self._rect.x, self._rect.y + 20)
self._back_button.render()
content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40)
self._scroller.render(content_rect)
def show_event(self):
self._current_panel = PanelType.SETTINGS
self._scroller.show_event()
self._speed_limit_mode.show_description(True)
def hide_event(self):
self._current_panel = PanelType.SETTINGS
self._scroller.hide_event()

View File

@@ -72,15 +72,6 @@ class SteeringLayout(Widget):
description="",
label_callback=lambda speed: f'{speed} {"km/h" if ui_state.is_metric else "mph"}',
)
self._blinker_reengage_delay = option_item_sp(
param="BlinkerLateralReengageDelay",
title=lambda: tr("Post-Blinker Delay"),
min_value=0,
max_value=10,
value_change_step=1,
description=lambda: tr("Delay before lateral control resumes after the turn signal ends."),
label_callback=lambda delay: f'{delay} {"s"}'
)
self._torque_control_toggle = toggle_item_sp(
param="EnforceTorqueControl",
title=lambda: tr("Enforce Torque Lateral Control"),
@@ -105,7 +96,6 @@ class SteeringLayout(Widget):
LineSeparatorSP(40),
self._blinker_control_toggle,
self._blinker_control_options,
self._blinker_reengage_delay,
LineSeparatorSP(40),
self._torque_control_toggle,
self._torque_customization_button,
@@ -138,7 +128,6 @@ class SteeringLayout(Widget):
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())
self._blinker_control_options.set_visible(self._blinker_control_toggle.action_item.get_state())
self._blinker_reengage_delay.set_visible(self._blinker_control_toggle.action_item.get_state())
enforce_torque_enabled = self._torque_control_toggle.action_item.get_state()
nnlc_enabled = self._nnlc_toggle.action_item.get_state()

View File

@@ -9,7 +9,6 @@ import pyray as rl
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import NavButton
@@ -113,7 +112,7 @@ class MadsSettingsLayout(Widget):
if self._mads_limited_settings():
ui_state.params.remove("MadsMainCruiseAllowed")
ui_state.params.put_bool("MadsUnifiedEngagementMode", True)
ui_state.params.put("MadsSteeringMode", MadsSteeringModeOnBrake.DISENGAGE)
ui_state.params.put("MadsSteeringMode", 2)
self._main_cruise_toggle.action_item.set_enabled(False)
self._main_cruise_toggle.action_item.set_state(False)
@@ -123,9 +122,9 @@ class MadsSettingsLayout(Widget):
self._unified_engagement_toggle.action_item.set_state(True)
self._unified_engagement_toggle.set_description("<b>" + DEFAULT_TO_ON + "</b><br>" + MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)
self._steering_mode.action_item.set_enabled(False)
self._steering_mode.set_description(STATUS_DISENGAGE_ONLY)
self._steering_mode.action_item.set_selected_button(MadsSteeringModeOnBrake.DISENGAGE)
self._steering_mode.action_item.set_enabled_buttons({MadsSteeringModeOnBrake.DISENGAGE})
self._steering_mode.action_item.set_selected_button(2)
else:
self._main_cruise_toggle.action_item.set_enabled(True)
self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC)
@@ -134,4 +133,3 @@ class MadsSettingsLayout(Widget):
self._unified_engagement_toggle.set_description(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC)
self._steering_mode.action_item.set_enabled(True)
self._steering_mode.action_item.set_enabled_buttons(None)

View File

@@ -355,10 +355,5 @@ class SunnylinkLayout(Widget):
def show_event(self):
super().show_event()
ui_state.sunnylink_state.set_settings_open(True)
self._scroller.show_event()
self._sunnylink_description.set_visible(False)
def hide_event(self):
super().hide_event()
ui_state.sunnylink_state.set_settings_open(False)

View File

@@ -6,14 +6,8 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.ui_state import UIStatus
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
LANE_LINE_COLORS_SP = {
UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255),
UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255),
}
class ModelRendererSP:
def __init__(self):
self.rainbow_path = RainbowPath()

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 ui_state.sm['controlsState'].lateralControlState.which() != 'angleState' and fade_alpha > 1e-2:
if ui_state.torque_bar and fade_alpha > 1e-2:
# Scale the fade texture to the content rect
rl.draw_texture_pro(self._fade_texture,
rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height),

View File

@@ -19,8 +19,8 @@ from openpilot.system.ui.widgets import Widget
class DeveloperUiRenderer(Widget):
DEV_UI_OFF = 0
DEV_UI_BOTTOM = 1
DEV_UI_RIGHT = 2
DEV_UI_RIGHT = 1
DEV_UI_BOTTOM = 2
DEV_UI_BOTH = 3
BOTTOM_BAR_HEIGHT = 61
@@ -62,10 +62,10 @@ class DeveloperUiRenderer(Widget):
if sm.recv_frame["carState"] < ui_state.started_frame:
return
if self.dev_ui_mode == self.DEV_UI_BOTTOM:
self._draw_bottom_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_RIGHT:
if self.dev_ui_mode == self.DEV_UI_RIGHT:
self._draw_right_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_BOTTOM:
self._draw_bottom_dev_ui(rect)
elif self.dev_ui_mode == self.DEV_UI_BOTH:
self._draw_right_dev_ui(rect)
self._draw_bottom_dev_ui(rect)

View File

@@ -6,8 +6,9 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel
@@ -16,11 +17,6 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartC
from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController
from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.speed_renderer import SpeedRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer, UI_CONFIG, FONT_SIZES, COLORS, CRUISE_DISABLED_CHAR
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
class HudRendererSP(HudRenderer):
@@ -36,21 +32,7 @@ class HudRendererSP(HudRenderer):
self.speed_renderer = SpeedRenderer()
self._torque_bar = TorqueBar(scale=3.0, always=True)
self.pcm_cruise_speed: bool = True
self.show_icbm_status: bool = False
self.icbm_active_counter: int = 0
self.speed_cluster: float = 0.0
self.speed_conv: float = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
def _update_state(self) -> None:
if ui_state.sm.recv_frame["carState"] < ui_state.started_frame:
return
if ui_state.CP_SP is not None:
self.pcm_cruise_speed = ui_state.CP_SP.pcmCruiseSpeed
self.speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
self.speed_cluster = ui_state.sm['carState'].cruiseState.speedCluster * self.speed_conv
super()._update_state()
self.road_name_renderer.update()
self.speed_limit_renderer.update()
@@ -59,64 +41,6 @@ class HudRendererSP(HudRenderer):
self.circular_alerts_renderer.update()
self.speed_renderer.update()
def _get_icbm_status(self):
if not self.pcm_cruise_speed and ui_state.sm['carControl'].enabled:
if round(self.set_speed) != round(self.speed_cluster):
self.icbm_active_counter = 3 * gui_app.target_fps # 3 seconds usually
elif self.icbm_active_counter > 0:
self.icbm_active_counter -= 1
else:
self.icbm_active_counter = 0
self.show_icbm_status = self.icbm_active_counter > 0
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
self._get_icbm_status()
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2
y = rect.y + 45
set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height)
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.BLACK_TRANSLUCENT)
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.BORDER_TRANSLUCENT)
max_color = COLORS.GREY
set_speed_color = COLORS.DARK_GREY
if self.is_cruise_set:
set_speed_color = COLORS.WHITE
if ui_state.status == UIStatus.ENGAGED:
max_color = COLORS.ENGAGED
elif ui_state.status == UIStatus.DISENGAGED:
max_color = COLORS.DISENGAGED
elif ui_state.status == UIStatus.OVERRIDE:
max_color = COLORS.OVERRIDE
max_str_size = 60 if self.show_icbm_status else 40
max_str_y = 15 if self.show_icbm_status else 27
max_text = str(round(self.speed_cluster)) if self.show_icbm_status else tr("MAX")
max_text_width = measure_text_cached(self._font_semi_bold, max_text, max_str_size).x
rl.draw_text_ex(
self._font_semi_bold,
max_text,
rl.Vector2(x + (set_speed_width - max_text_width) / 2, y + max_str_y),
max_str_size,
0,
max_color,
)
set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed))
speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x
rl.draw_text_ex(
self._font_bold,
set_speed_text,
rl.Vector2(x + (set_speed_width - speed_text_width) / 2, y + 77),
FONT_SIZES.set_speed,
0,
set_speed_color,
)
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
self.speed_renderer.render(rect)

View File

@@ -72,28 +72,72 @@ class TurnSignalWidget(Widget):
class TurnSignalController:
def __init__(self):
self._config = TurnSignalConfig()
def __init__(self, config: TurnSignalConfig | None = None):
self._config = config or TurnSignalConfig()
self._left_signal = TurnSignalWidget(direction=IconSide.left)
self._right_signal = TurnSignalWidget(direction=IconSide.right)
@staticmethod
def _update_signal(signal, blindspot, blinker):
if ui_state.blindspot and blindspot:
signal.activate('blind_spot')
elif ui_state.turn_signals and blinker:
signal.activate('signal')
else:
signal.deactivate()
self._last_icon_side = None
def update(self):
CS = ui_state.sm['carState']
sm = ui_state.sm
ss = sm['selfdriveState']
self._update_signal(self._left_signal, CS.leftBlindspot, CS.leftBlinker)
self._update_signal(self._right_signal, CS.rightBlindspot, CS.rightBlinker)
event_name = ss.alertType.split('/')[0] if ss.alertType else ''
if event_name == 'preLaneChangeLeft':
self._last_icon_side = IconSide.left
self._left_signal.activate('signal')
self._right_signal.deactivate()
elif event_name == 'preLaneChangeRight':
self._last_icon_side = IconSide.right
self._right_signal.activate('signal')
self._left_signal.deactivate()
elif event_name == 'laneChange':
if self._last_icon_side == IconSide.left:
self._left_signal.activate('signal')
self._right_signal.deactivate()
elif self._last_icon_side == IconSide.right:
self._right_signal.activate('signal')
self._left_signal.deactivate()
elif event_name == 'laneChangeBlocked':
CS = sm['carState']
if CS.leftBlinker:
icon_side = IconSide.left
elif CS.rightBlinker:
icon_side = IconSide.right
else:
icon_side = self._last_icon_side
if icon_side == IconSide.left:
self._left_signal.activate('blind_spot')
self._right_signal.deactivate()
elif icon_side == IconSide.right:
self._right_signal.activate('blind_spot')
self._left_signal.deactivate()
else:
self._last_icon_side = None
CS = sm['carState']
if CS.leftBlindspot:
self._left_signal.activate('blind_spot')
elif CS.leftBlinker:
self._left_signal.activate('signal')
else:
self._left_signal.deactivate()
if CS.rightBlindspot:
self._right_signal.activate('blind_spot')
elif CS.rightBlinker:
self._right_signal.activate('signal')
else:
self._right_signal.deactivate()
def render(self, rect: rl.Rectangle):
if not ui_state.turn_signals and not ui_state.blindspot:
if not ui_state.turn_signals:
return
x = rect.x + rect.width / 2

View File

@@ -26,7 +26,6 @@ class OnroadTimerStatus(Enum):
class UIStateSP:
def __init__(self):
self.CP_SP: custom.CarParamsSP | None = None
self.params = Params()
self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
@@ -39,9 +38,6 @@ class UIStateSP:
self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.reset_onroad_sleep_timer()
self.CP_SP: custom.CarParamsSP | None = None
self.has_icbm: bool = False
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
def update(self) -> None:
if self.sunnylink_enabled:
@@ -124,7 +120,6 @@ class UIStateSP:
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
if CP_SP_bytes is not None:
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
self.blindspot = self.params.get_bool("BlindSpot")
self.chevron_metrics = self.params.get("ChevronInfo")
@@ -142,14 +137,15 @@ class UIStateSP:
self.torque_bar = self.params.get_bool("TorqueBar")
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
self.turn_signals = self.params.get_bool("ShowTurnSignals")
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
class DeviceSP:
@staticmethod
def _set_awake(on: bool, _ui_state):
if _ui_state.boot_offroad_mode == 1 and not on:
_ui_state.params.put_bool("OffroadMode", True)
def __init__(self):
self._params = Params()
def _set_awake(self, on: bool):
if on and self._params.get("DeviceBootMode", return_default=True) == 1:
self._params.put_bool("OffroadMode", True)
@staticmethod
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:

View File

@@ -3,6 +3,7 @@ import os
import sys
import subprocess
import tempfile
import base64
import webbrowser
import argparse
from pathlib import Path
@@ -24,6 +25,12 @@ def compare_frames(frame1_path, frame2_path):
return result.returncode == 0
def frame_to_data_url(frame_path):
with open(frame_path, 'rb') as f:
data = f.read()
return f"data:image/png;base64,{base64.b64encode(data).decode()}"
def create_diff_video(video1, video2, output_path):
"""Create a diff video using ffmpeg blend filter with difference mode."""
print("Creating diff video...")
@@ -53,16 +60,20 @@ def find_differences(video1, video2):
print(f"Comparing {len(frames1)} frames...")
different_frames = []
frame_data = []
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
is_different = not compare_frames(f1, f2)
if is_different:
different_frames.append(i)
return different_frames, len(frames1)
if i < 10 or i >= len(frames1) - 10 or is_different:
frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)})
return different_frames, frame_data, len(frames1)
def generate_html_report(video1, video2, basedir, different_frames, total_frames):
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
chunks = []
if different_frames:
current_chunk = [different_frames[0]]
@@ -166,14 +177,14 @@ def main():
diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4")
create_diff_video(args.video1, args.video2, diff_video_path)
different_frames, total_frames = find_differences(args.video1, args.video2)
different_frames, frame_data, total_frames = find_differences(args.video1, args.video2)
if different_frames is None:
sys.exit(1)
print()
print("Generating HTML report...")
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, total_frames)
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames)
with open(DIFF_OUT_DIR / args.output, 'w') as f:
f.write(html)

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
"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(on, ui_state)
DeviceSP._set_awake(self, on)
self._awake = on
cloudlog.debug(f"setting display power {int(on)}")
HARDWARE.set_display_power(on)

View File

@@ -2,3 +2,4 @@ SConscript(['common/transformations/SConscript'])
SConscript(['modeld/SConscript'])
SConscript(['modeld_v2/SConscript'])
SConscript(['selfdrive/locationd/SConscript'])
SConscript(['modeld_sunny/SConscript'])

View File

@@ -0,0 +1,32 @@
import os
import glob
Import('env', 'arch')
lenv = env.Clone()
tinygrad_repo = env.Dir("#tinygrad_repo")
tinygrad_files = ["#"+x for x in glob.glob(tinygrad_repo.relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]
def mayo_compile(flags, model_name):
pythonpath_string = f'PYTHONPATH="${{PYTHONPATH}}:{tinygrad_repo.abspath}"'
onnx_fn = f"distilled_models/{model_name}.onnx"
pkl_fn = f"distilled_models/{model_name}_tinygrad.pkl"
if not os.path.exists("distilled_models"):
try:
os.makedirs("distilled_models")
except OSError:
pass
if os.path.isfile(File(onnx_fn).abspath):
return lenv.Command(
pkl_fn,
[onnx_fn, "compile_split_tinygrad.py"] + tinygrad_files,
f'{pythonpath_string} {flags} python3 {File("compile_split_tinygrad.py").abspath} {File(onnx_fn).abspath} {File(pkl_fn).abspath}'
)
flags = {
'larch64': 'DEV=QCOM',
'Darwin': f'DEV=METAL HOME={os.path.expanduser("~")} IMAGE=0',
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
for m in ["student_vision", "student_policy"]:
mayo_compile(flags, m)

View File

View File

@@ -0,0 +1,109 @@
"""
The whole point of this module is to mimic compile3.py while adapting it for our buffers to prevent buffer explosion
"""
import os
import sys
import pickle
from tinygrad import Tensor, TinyJit, Context, Device
from tinygrad.device import Buffer
# from tinygrad.nn.onnx import OnnxRunner
from tinygrad.frontend.onnx import OnnxRunner
if "JIT_BATCH_SIZE" not in os.environ:
os.environ["JIT_BATCH_SIZE"] = "0"
if "FLOAT16" not in os.environ:
os.environ["FLOAT16"] = "1"
if "OPT" not in os.environ:
os.environ["OPT"] = "99"
KEEP_BUFFERS = set()
original_reduce = Buffer.__reduce__
def stripped_reduce(self):
if id(self) in KEEP_BUFFERS:
return original_reduce(self)
return (self.__class__, (self.device, self.size, self.dtype))
Buffer.__reduce__ = stripped_reduce
def compile_model(onnx_path, output_path, input_shapes=None, input_types=None):
print(f"Compiling {onnx_path} -> {output_path}")
run_onnx = OnnxRunner(onnx_path)
if input_shapes is None:
input_shapes = {name: spec.shape for name, spec in run_onnx.graph_inputs.items()}
if input_types is None:
input_types = {name: spec.dtype for name, spec in run_onnx.graph_inputs.items()}
Tensor.manual_seed(100)
inputs = {k: Tensor(Tensor.randn(*shp, dtype=input_types[k]).mul(8).realize().numpy(), device='NPY') for k, shp in sorted(input_shapes.items())}
inputs = {k: v.to(Device.DEFAULT).realize() for k, v in inputs.items()}
print(f"Realized all {len(inputs)} inputs on {Device.DEFAULT}")
input_buf_ids = set()
for _, v in inputs.items():
if hasattr(v, '_buffer'):
try:
b = v._buffer()
if b is not None:
input_buf_ids.add(id(b))
except Exception:
pass
if "vision" in onnx_path:
onnx_jit = TinyJit(lambda **kwargs: next(iter(run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values())).cast('float32'), prune=True)
else:
onnx_jit = TinyJit(lambda **kwargs: [x.cast('float32').contiguous().realize() for x in run_onnx({k:v.to(Device.DEFAULT) for k,v in kwargs.items()}).values()], prune=True)
for i in range(3):
with Context(DEBUG=max(int(os.getenv("DEBUG", 0)), 2 if i == 2 else 1)):
res = onnx_jit(**inputs)
if isinstance(res, list):
for x in res:
x.numpy()
else:
res.numpy()
print(f"Captured {len(onnx_jit.captured.jit_cache)} kernels")
all_read_ids = set()
all_written_ids = set()
for ji in onnx_jit.captured.jit_cache:
if len(ji.bufs) > 0:
if ji.bufs[0] is not None:
all_written_ids.add(id(ji.bufs[0]))
for b in ji.bufs[1:]:
if b is not None:
all_read_ids.add(id(b))
weight_candidates = all_read_ids - all_written_ids
weight_ids = weight_candidates - input_buf_ids
print(f"Identified {len(weight_ids)} weight candidates (Read-Only & Not Input).")
total_weight_size = 0
marked_count = 0
for ji in onnx_jit.captured.jit_cache:
for b in ji.bufs:
if b is not None and id(b) in weight_ids:
if id(b) not in KEEP_BUFFERS:
KEEP_BUFFERS.add(id(b))
total_weight_size += b.size * b.dtype.itemsize
marked_count += 1
print(f"Preserving {marked_count} unique weight buffers.")
print(f"Total Preserved Weight Data Size: {total_weight_size / 1e6:.2f} MB")
with open(output_path, "wb") as f:
pickle.dump(onnx_jit, f)
print(f"Saved {output_path}, pkl size: {os.path.getsize(output_path)/1e6:.2f} MB")
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python compile_split_tinygrad.py <input_onnx> <output_pkl>")
sys.exit(1)
input_onnx = sys.argv[1]
output_pkl = sys.argv[2]
compile_model(input_onnx, output_pkl)

View File

@@ -0,0 +1,161 @@
import numpy as np
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, get_curvature_from_plan
def interp_vec(t_out, t_in, vec):
# vec shape (N, 3), output (M, 3)
return np.stack([np.interp(t_out, t_in, vec[:, i]) for i in range(3)], axis=1)
def fill_alpamayo_msg(modelV2, net_outputs, frame_id, frame_drop_ratio, timestamp_eof, CP, lat_delay, v_ego):
modelV2.frameId = frame_id
modelV2.frameIdExtra = frame_id
modelV2.timestampEof = timestamp_eof
modelV2.frameDropPerc = frame_drop_ratio * 100.0
modelV2.init('laneLines', 4)
modelV2.init('roadEdges', 2)
modelV2.init('laneLineProbs', 4)
modelV2.init('roadEdgeStds', 2)
for i in range(4):
l = modelV2.laneLines[i]
l.t = [0.0]
l.x = [0.0]
l.y = [0.0]
l.z = [0.0]
modelV2.laneLineProbs[i] = 0.0
for i in range(2):
e = modelV2.roadEdges[i]
e.t = [0.0]
e.x = [0.0]
e.y = [0.0]
e.z = [0.0]
modelV2.roadEdgeStds[i] = 1.0
leads = modelV2.init('leadsV3', 1)
lead = leads[0]
pred_lead = net_outputs['pred_lead'][0]
prob_logit = float(pred_lead[0])
dist_pred = float(pred_lead[1] * 100.0)
dist_sigma = float(np.exp(pred_lead[2]))
v_rel_pred = float(pred_lead[3])
v_sigma = float(np.exp(pred_lead[4]))
a_rel_pred = float(pred_lead[5])
a_sigma = float(np.exp(pred_lead[6]))
prob = float(1.0 / (1.0 + np.exp(-prob_logit)))
lead.prob = prob
lead.probTime = 0.0
# X(t) = X0 + V_rel*t + 0.5*A_rel*t^2
T = ModelConstants.T_IDXS
lead.t = list(T)
lead.x = [float(dist_pred + v_rel_pred * t + 0.5 * a_rel_pred * t**2) for t in T]
lead.v = [float(v_ego + v_rel_pred + a_rel_pred * t) for t in T]
a_ego = net_outputs['acceleration'][0, 0, 0] # T=0 ego accel estimate (x component)
lead.a = [float(a_ego + a_rel_pred)] * len(T)
lead.y = [0.0] * len(T)
lead.xStd = [max(0.5, dist_sigma * 100.0)] * len(T)
lead.yStd = [1.0] * len(T)
lead.vStd = [max(0.1, v_sigma)] * len(T)
lead.aStd = [max(0.1, a_sigma)] * len(T)
modelV2.meta.engagedProb = 1.0
desire_pred = [0.0] * 8
if 'pred_light' in net_outputs:
red_prob = float(1.0 / (1.0 + np.exp(-net_outputs['pred_light'][0, 1] + net_outputs['pred_light'][0, 0])))
desire_pred[4] = red_prob
modelV2.meta.desirePrediction = desire_pred
modelV2.meta.desireState = [0.0] * 8
reasoning_error = net_outputs.get('consistency_error', 0.0)
if reasoning_error < 0.5:
modelV2.confidence = "green"
elif reasoning_error < 1.5:
modelV2.confidence = "yellow"
else:
modelV2.confidence = "red"
ALPAMAYO_T_IDXS = np.arange(1, 65) * 0.1 # 64 steps at .1s intervals
t_idxs = ModelConstants.T_IDXS
t_all = np.concatenate(([0.0], ALPAMAYO_T_IDXS)) # this model starts at t=0.1 so if we prepend 0.0 and interpolate for t=now it should match op
pos_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs['position'][0])))
pos_std_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs.get('position_std', np.ones((64, 3)) * 0.1))))
vel_interp = interp_vec(t_idxs, t_all, np.vstack(([v_ego, 0.0, 0.0], net_outputs['velocity'][0])))
acc_interp = interp_vec(t_idxs, t_all, np.vstack((net_outputs['acceleration'][0][0], net_outputs['acceleration'][0])))
rot_interp = interp_vec(t_idxs, t_all, np.vstack((np.zeros(3), net_outputs['orientation'][0])))
rate_interp = interp_vec(t_idxs, t_all, np.vstack((net_outputs['orientation_rate'][0][0], net_outputs['orientation_rate'][0])))
# https://www.mathworks.com/help/vdynblks/ug/coordinate-systems-in-vehicle-dynamics-blockset.html
# following SAE J670 and ISO 8855, for sunnymayo model: x is forward (f), y is left (lat), z is up/vert
# Openpilot Modelv2 and camerad expects SAE J670: x is forward, y is right, z is down
modelV2.position.t = t_idxs # time, obviously
modelV2.position.x = pos_interp[:, 0].tolist() # f dist
modelV2.position.y = (-pos_interp[:, 1]).tolist() # lat offset (Flip L->R)
modelV2.position.z = (-pos_interp[:, 2]).tolist() # vert offset (Flip U->D) (elevation)
modelV2.position.xStd = pos_std_interp[:, 0].tolist()
modelV2.position.yStd = pos_std_interp[:, 1].tolist()
modelV2.position.zStd = pos_std_interp[:, 2].tolist()
modelV2.velocity.t = t_idxs
modelV2.velocity.x = vel_interp[:, 0].tolist() # f vel (vego)
modelV2.velocity.y = (-vel_interp[:, 1]).tolist() # lat vel (curvature)
modelV2.velocity.z = (-vel_interp[:, 2]).tolist() # vert vel
modelV2.acceleration.t = t_idxs
modelV2.acceleration.x = acc_interp[:, 0].tolist() # f accel (aego)
modelV2.acceleration.y = (-acc_interp[:, 1]).tolist() # lat accel
modelV2.acceleration.z = (-acc_interp[:, 2]).tolist() # vert accel
modelV2.orientation.t = t_idxs
modelV2.orientation.x = rot_interp[:, 0].tolist() # roll (treated as 0)
modelV2.orientation.y = (-rot_interp[:, 1]).tolist() # pitch (from z-slope)
modelV2.orientation.z = (-rot_interp[:, 2]).tolist() # yaw (heading)
modelV2.orientationRate.t = t_idxs
modelV2.orientationRate.x = rate_interp[:, 0].tolist() # roll rate
modelV2.orientationRate.y = (-rate_interp[:, 1]).tolist() # pitch rate (Flip U->D)
modelV2.orientationRate.z = (-rate_interp[:, 2]).tolist() # yaw rate (Flip L->R)
long_action_t = CP.longitudinalActuatorDelay + DT_MDL
desired_accel, should_stop = get_accel_from_plan(vel_interp[:, 0], acc_interp[:, 0], t_idxs, action_t=long_action_t)
modelV2.action.desiredAcceleration = float(desired_accel)
modelV2.action.shouldStop = bool(should_stop)
lat_action_t = lat_delay + DT_MDL
desired_curvature = get_curvature_from_plan(-rot_interp[:, 2], -rate_interp[:, 2], t_idxs, vego=v_ego, action_t=lat_action_t)
modelV2.action.desiredCurvature = float(desired_curvature)
def fill_pose_msg(camera_odometry, net_outputs, frame_id, timestamp_eof):
camera_odometry.frameId = frame_id
camera_odometry.timestampEof = timestamp_eof
trans = net_outputs['velocity'][0, 0].copy()
trans[1] *= -1.0
trans[2] *= -1.0
camera_odometry.trans = trans.tolist()
std_val = float(max(0.01, net_outputs.get('consistency_error', 0.1)))
camera_odometry.transStd = [std_val, std_val, std_val]
rot = net_outputs['orientation_rate'][0, 0].copy()
rot[1] *= -1.0
rot[2] *= -1.0
camera_odometry.rot = rot.tolist()
rot_std = std_val * 0.1
camera_odometry.rotStd = [rot_std, rot_std, rot_std]

View File

@@ -0,0 +1,51 @@
import numpy as np
from dataclasses import dataclass
from openpilot.common.params import Params
@dataclass
class AlpamayoDesire:
DRIVE_SAFELY = 0
TURN_LEFT = 2
TURN_RIGHT = 1
DRIVE_FAST = 3
STOP = 4
class InputIDHelper:
def __init__(self):
self.current_ids = np.zeros((1, 16), dtype=np.int64)
self.desire = AlpamayoDesire.DRIVE_SAFELY
self.params = Params()
self.drive_fast = False
self.msg_count = -1
def update_params(self):
if self.msg_count % 60 == 0:
self.drive_fast = self.params.get_bool("AlpamayoDriveFast")
self.msg_count += 1
def update(self, sm):
self.update_params()
if sm is None:
return self.current_ids
left_blinker = False
right_blinker = False
if sm.seen['carState']:
left_blinker = sm['carState'].leftBlinker
right_blinker = sm['carState'].rightBlinker
# Priority: STOP (TODO) > Turn > Drive Fast > Drive Safely
new_desire = AlpamayoDesire.DRIVE_SAFELY
if left_blinker:
new_desire = AlpamayoDesire.TURN_LEFT
elif right_blinker:
new_desire = AlpamayoDesire.TURN_RIGHT
elif self.drive_fast:
new_desire = AlpamayoDesire.DRIVE_FAST
self.desire = new_desire
self.current_ids.fill(self.desire)
return self.current_ids

View File

@@ -0,0 +1,60 @@
from tinygrad.tensor import Tensor
def action_to_traj(action: Tensor, v0: Tensor, dt: float = 0.1):
"""
This function is a lightweight tinygrad transformation of the unicycle accel physics model based on Nvidia's
unicycle model https://github.com/NVlabs/alpamayo/blob/main/src/alpamayo_r1/action_space/unicycle_accel_curvature.py
Integrate action (accel, kappa) to trajectory (x, y, theta)
Args:
action: (B, T, 2) [accel, kappa]
v0: (B,) Initial velocity
dt: Time step
Returns:
res: Dict containing position, velocity, acceleration, orientation, orientation_rate
"""
B, T, _ = action.shape
ACCEL_MEAN = 0.02902695
ACCEL_STD = 0.68104267
CURV_MEAN = 0.00026922
CURV_STD = 0.02614828
accel = action[..., 0] * ACCEL_STD + ACCEL_MEAN
kappa = action[..., 1] * CURV_STD + CURV_MEAN
# v_{t+1} = v_t + a_t * dt
v_diff = accel * dt
v_seq = v_diff.cumsum(axis=1) + v0.reshape(B, 1) # cumulative sum over T dimension (axis 1)
velocity = v0.reshape(B, 1).cat(v_seq, dim=1)
# theta_{t+1} = theta_t + kappa_t * (v_t * dt + 0.5 * a_t * dt^2)
dt_2_term = 0.5 * (dt**2)
dtheta = kappa * (velocity[:, :-1] * dt + accel * dt_2_term)
theta_seq = dtheta.cumsum(axis=1)
theta = Tensor.zeros(B, 1, device=action.device, dtype=action.dtype).cat(theta_seq, dim=1)
# trapezoidal euler
half_dt = 0.5 * dt
v_cos = velocity * theta.cos()
v_sin = velocity * theta.sin()
dx = (v_cos[:, :-1] + v_cos[:, 1:]) * half_dt
dy = (v_sin[:, :-1] + v_sin[:, 1:]) * half_dt
x = dx.cumsum(axis=1)
y = dy.cumsum(axis=1)
res = {}
res['action'] = accel.stack(kappa, dim=-1) # raw model output
# (x, y, 0)
res['position'] = x.stack(y, Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
# (vx, vy, 0)
res['velocity'] = v_cos[:, 1:].stack(v_sin[:, 1:], Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
# ax = accel * cos(theta), ay = accel * sin(theta), 0
res['acceleration'] = (accel * theta[:, 1:].cos()).stack(accel * theta[:, 1:].sin(), Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dim=-1)
# (0, 0, theta)
res['orientation'] = Tensor.zeros(B, T, device=action.device, dtype=action.dtype).stack(Tensor.zeros(B, T, device=action.device, dtype=action.dtype), theta[:, 1:], dim=-1)
# (0, 0, dtheta/dt)
res['orientation_rate'] = Tensor.zeros(B, T, device=action.device, dtype=action.dtype).stack(Tensor.zeros(B, T, device=action.device, dtype=action.dtype), dtheta / dt, dim=-1)
return res

View File

@@ -0,0 +1,19 @@
import pickle
from pathlib import Path
from openpilot.common.swaglog import cloudlog
def load_compiled_model(model_name: str = "student"):
pkl_path = Path(__file__).parent / "distilled_models" / f"{model_name}_tinygrad.pkl"
if not pkl_path.exists():
cloudlog.error(f"Compiled model not found at {pkl_path}")
return None
try:
with open(pkl_path, "rb") as f:
model_run = pickle.load(f)
return model_run
except Exception as e:
cloudlog.error(f"Failed to load compiled Tinygrad model: {e}")
return None

View File

@@ -0,0 +1,344 @@
import time
import numpy as np
import os
import platform
from setproctitle import setproctitle
from openpilot.system.hardware import TICI
if TICI:
os.environ['DEV'] = 'QCOM'
elif platform.system() == "Darwin":
os.environ['DEV'] = "METAL"
else:
os.environ['DEV'] = 'CPU'
USBGPU = "USBGPU" in os.environ
if USBGPU:
os.environ['DEV'] = 'AMD'
os.environ['AMD_IFACE'] = 'USB'
from tinygrad.tensor import Tensor
from tinygrad.dtype import dtypes
import cereal.messaging as messaging
from cereal import car
from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType
from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.transformations.model import bigmodel_frame_from_calib_frame
from openpilot.common.transformations.camera import DEVICE_CAMERAS, view_frame_from_device_frame
from openpilot.common.transformations.orientation import rot_from_euler
from openpilot.common.realtime import Ratekeeper
from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext
from openpilot.sunnypilot.modeld_sunny.kinematic_model import action_to_traj
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.modeld_sunny.loader import load_compiled_model
from openpilot.sunnypilot.modeld_sunny.input_id_helper import InputIDHelper
from openpilot.sunnypilot.modeld_sunny.fill_model_msg import fill_alpamayo_msg, fill_pose_msg
PROCESS_NAME = "selfdrive.modeld.openpilot.sunnypilot.modeld_sunny"
class FrameMeta:
frame_id: int = 0
timestamp_sof: int = 0
timestamp_eof: int = 0
def __init__(self, vipc=None):
if vipc is not None:
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
def safe_exp(x):
return np.exp(np.clip(x, -np.inf, 11))
def softmax(x, axis=-1):
x = x - np.max(x, axis=axis, keepdims=True)
x = safe_exp(x)
return x / np.sum(x, axis=axis, keepdims=True)
class AlpamayoModelD:
def __init__(self, context: CLContext):
self.params = Params()
self.context = context
self.model_vision = load_compiled_model("student_vision")
self.model_policy = load_compiled_model("student_policy")
self.model_loaded = self.model_vision is not None and self.model_policy is not None
self.vision_input_names = ['road', 'wide']
self.vision_input_shapes = {
'road': (1, 3, 512, 1024),
'wide': (1, 3, 512, 1024)
}
self.frames = {name: DrivingModelFrame(context, 1024, 512, buffer_length=4) for name in self.vision_input_names}
self.history_buffer = np.zeros((16, 3), dtype=np.float32)
self.logic_pulse = np.zeros((1, 2048), dtype=np.float32)
def run(self, bufs, transforms, inputs, prepare_only):
if prepare_only:
return None
if not hasattr(self, 'vision_inputs'):
self.vision_inputs = {}
imgs_cl = {n: self.frames[n].prepare(bufs[n], transforms[n].flatten()) for n in self.vision_input_names if bufs.get(n)}
if TICI and not USBGPU:
for k, v in imgs_cl.items():
if k not in self.vision_inputs:
self.vision_inputs[k] = qcom_tensor_from_opencl_address(v.mem_address, self.vision_input_shapes[k], dtype=dtypes.uint8)
else:
for k, v in imgs_cl.items():
self.vision_inputs[k] = Tensor(self.frames[k].buffer_from_cl(v).reshape(self.vision_input_shapes[k]), dtype=dtypes.uint8).realize()
img_t = Tensor.stack([self.vision_inputs['wide'].cast(dtypes.float32) / 255.0,
self.vision_inputs['road'].cast(dtypes.float32) / 255.0], dim=1).unsqueeze(0)
vis_res = self.model_vision(
history=Tensor(inputs["history"]).contiguous().realize(),
img=img_t.contiguous().realize(),
input_ids=Tensor(inputs["input_ids"]).contiguous().realize(),
logic_pulse=Tensor(inputs["logic_pulse"]).contiguous().realize()
)
context = vis_res.contiguous().realize()
x_input = Tensor.zeros(1, 64, 2, device=os.environ.get("DEV"), dtype=dtypes.float32)
v_mu, v_std, pred_pulse, state_mu, state_std, pred_light, pred_lead, hypot_logits = self.model_policy(
context=context,
noisy_action=x_input.contiguous().realize(),
t=Tensor(np.array([[0.0]], dtype=np.float32)).contiguous().realize(), # t=0
traffic=Tensor(inputs["traffic_convention"]).contiguous().realize()
)
weights = softmax(hypot_logits.numpy(), axis=1) # (B, M)
winner_idx = np.argmax(weights[0])
v_winner = v_mu[:, winner_idx]
state_winner = state_mu[:, winner_idx]
state_std_winner = state_std[:, winner_idx]
outputs_tg = action_to_traj(v_winner, Tensor([inputs["v_ego"]], dtype=dtypes.float32), dt=0.1)
outputs = {k: v.numpy() for k, v in outputs_tg.items()}
outputs.update({
"pred_pulse": pred_pulse.numpy(),
"pred_light": pred_light[0:1].numpy(),
"pred_lead": pred_lead[0:1].numpy(),
"weights": weights[0]
})
# Inject world positions for Z/Pitch
pos_world = state_winner[0].numpy()
pos_std = np.exp(state_std_winner[0].numpy())
outputs["position"][0, :, 2] = pos_world[:, 2]
outputs["position_std"] = pos_std # log_sigma -> sigma
d_pos = np.diff(outputs["position"][0], axis=0, prepend=np.zeros((1, 3)))
d_dist = np.maximum(np.linalg.norm(d_pos[:, :2], axis=1), 1e-4)
pitch = np.arctan2(np.diff(pos_world[:, 2], prepend=0.0), d_dist)
outputs["orientation"][0, :, 1] = pitch
outputs["orientation_rate"][0, :, 1] = np.diff(pitch, prepend=0.0) / 0.1
outputs["velocity"][0, :, 2] = np.linalg.norm(outputs["velocity"][0, :, :2], axis=1) * np.tan(pitch)
outputs["consistency_error"] = float(np.mean(np.linalg.norm(outputs["position"][0] - pos_world, axis=1)))
return outputs
def main():
setproctitle(PROCESS_NAME)
config_realtime_process(7, 54)
# Loop runs at 20Hz to match camera acquisition.
# Model inference runs at 10Hz via frame skipping.
rk = Ratekeeper(1.0 / DT_MDL)
cl_context = CLContext()
modeld = AlpamayoModelD(cl_context)
# Load CarParams
cloudlog.warning("Modeld: Waiting for CarParams...")
CP = messaging.log_from_bytes(Params().get("CarParams", block=True), car.CarParams)
cloudlog.warning("Modeld: Got CarParams")
camera_offset_helper = CameraOffsetHelper()
input_id_helper = InputIDHelper()
if modeld.model_loaded:
cloudlog.warning("Modeld: Successfully loaded compiled student model.")
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "liveDelay", "livePose", "driverMonitoringState"])
# VisionIPC Clients
while True:
available_streams = VisionIpcClient.available_streams("camerad", block=False)
if available_streams:
use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams
main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams
break
time.sleep(.1)
vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD
vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True, cl_context)
vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context)
cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}")
while not vipc_client_main.connect(False):
time.sleep(0.1)
while use_extra_client and not vipc_client_extra.connect(False):
time.sleep(0.1)
cloudlog.warning(f"connected main cam with buffer size: {vipc_client_main.buffer_len} ({vipc_client_main.width} x {vipc_client_main.height})")
if use_extra_client:
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
model_transform_main = np.zeros((3, 3), dtype=np.float32)
model_transform_extra = np.zeros((3, 3), dtype=np.float32)
buf_main, buf_extra = None, None
meta_main = FrameMeta()
meta_extra = FrameMeta()
# filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / 20.0)
last_vipc_frame_id = 0
run_count = 0
lat_delay = 0.0
live_calib_seen = False
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
while meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
buf_main = vipc_client_main.recv()
meta_main = FrameMeta(vipc_client_main)
if buf_main is None:
break
if buf_main is None:
cloudlog.debug("vipc_client_main no frame")
continue
if use_extra_client:
# Keep receiving extra frames until frame id matches main camera
while True:
buf_extra = vipc_client_extra.recv()
meta_extra = FrameMeta(vipc_client_extra)
if buf_extra is None or meta_main.timestamp_sof < meta_extra.timestamp_sof + 25000000:
break
if buf_extra is None:
cloudlog.debug("vipc_client_extra no frame")
continue
if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000:
cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\
extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})")
else:
# Use single camera
buf_extra = buf_main
meta_extra = meta_main
# 10Hz Execution Check (Skip odd frames)
# We use main camera frameId as the clock
if meta_main.frame_id % 2 != 0:
last_vipc_frame_id = meta_main.frame_id
continue
sm.update(0)
v_ego = sm['carState'].vEgo if sm.seen['carState'] else 0.0
yaw_rate = 0.0
if sm.seen['livePose'] and sm['livePose'].angularVelocityDevice.valid:
yaw_rate = sm['livePose'].angularVelocityDevice.z
if sm.frame % 60 == 0:
lat_delay = get_lat_delay(modeld.params, sm["liveDelay"].lateralDelay)
camera_offset_helper.set_offset(modeld.params.get("CameraOffset", return_default=True))
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
calib_from_bigmodel = np.linalg.inv(bigmodel_frame_from_calib_frame[:, :3])
device_from_calib = rot_from_euler(device_from_calib_euler)
camera_from_calib_main = (dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics) @ view_frame_from_device_frame @ device_from_calib
model_transform_main = camera_from_calib_main @ calib_from_bigmodel
camera_from_calib_extra = dc.ecam.intrinsics @ view_frame_from_device_frame @ device_from_calib
model_transform_extra = camera_from_calib_extra @ calib_from_bigmodel
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
live_calib_seen = True
# Track dropped frames
vipc_dropped_frames = max(0, meta_main.frame_id - last_vipc_frame_id - 1)
frames_dropped = frame_dropped_filter.update(min(vipc_dropped_frames, 10))
if run_count < 10: # let frame drops warm up
frame_dropped_filter.x = 0.
frames_dropped = 0.
run_count = run_count + 1
frame_drop_ratio = frames_dropped / (1 + frames_dropped)
prepare_only = vipc_dropped_frames > 0
if prepare_only:
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
bufs = {'road': buf_main, 'wide': buf_extra}
transforms = {'road': model_transform_main, 'wide': model_transform_extra}
dt = 0.1
d_yaw = yaw_rate * dt
d_pos = v_ego * dt * np.array([np.cos(d_yaw/2), np.sin(d_yaw/2)])
rot = np.array([[np.cos(-d_yaw), -np.sin(-d_yaw)], [np.sin(-d_yaw), np.cos(-d_yaw)]])
modeld.history_buffer[:, :2] = (modeld.history_buffer[:, :2] - d_pos) @ rot.T
modeld.history_buffer[:, 2] -= d_yaw
modeld.history_buffer = np.roll(modeld.history_buffer, -1, axis=0)
modeld.history_buffer[-1] = 0.
hist_input = modeld.history_buffer.copy()
hist_input[:, 1] *= -1.0
hist_input[:, 2] *= -1.0
yaws_fixed = hist_input[:, 2]
cos_y, sin_y = np.cos(yaws_fixed), np.sin(yaws_fixed)
zeros, ones = np.zeros_like(cos_y), np.ones_like(cos_y)
rot_flat = np.column_stack([cos_y, -sin_y, zeros, sin_y, cos_y, zeros, zeros, zeros, ones])
inputs = {
'input_ids': input_id_helper.update(sm),
'history': np.column_stack([hist_input[:, :2], zeros, rot_flat])[None, ...].astype(np.float32),
'logic_pulse': modeld.logic_pulse,
'traffic_convention': np.array([[0.0, 1.0]] if sm["driverMonitoringState"].isRHD else [[1.0, 0.0]], dtype=np.float32),
'v_ego': v_ego
}
t0 = time.monotonic()
outputs = modeld.run(bufs, transforms, inputs, prepare_only)
t1 = time.monotonic()
if not prepare_only:
cloudlog.warning(f"Modeld: Inference took {(t1-t0)*1000:.2f} ms")
last_vipc_frame_id = meta_main.frame_id
if outputs is not None:
modeld.logic_pulse[:] = outputs["pred_pulse"]
model_msg = messaging.new_message('modelV2')
drivingdata_msg = messaging.new_message('drivingModelData')
posenet_msg = messaging.new_message('cameraOdometry')
fill_alpamayo_msg(model_msg.modelV2, outputs, meta_main.frame_id, frame_drop_ratio, meta_main.timestamp_eof, CP, lat_delay, v_ego)
model_msg.valid = live_calib_seen and (vipc_dropped_frames < 1)
fill_pose_msg(posenet_msg.cameraOdometry, outputs, meta_main.frame_id, meta_main.timestamp_eof)
posenet_msg.valid = live_calib_seen and (vipc_dropped_frames < 1)
drivingdata_msg.drivingModelData.frameId = meta_main.frame_id
pm.send('drivingModelData', drivingdata_msg)
pm.send('cameraOdometry', posenet_msg)
pm.send('modelV2', model_msg)
rk.keep_time()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
import pytest
import numpy as np
from cereal import car
import cereal.messaging as messaging
from msgq.visionipc import VisionIpcServer, VisionIpcClient, VisionStreamType
from sunnypilot.modeld_sunny.modeld import AlpamayoModelD
from sunnypilot.modeld_v2.models.commonmodel_pyx import CLContext
from sunnypilot.modeld_sunny.fill_model_msg import fill_alpamayo_msg
@pytest.fixture(scope="module")
def cl_context():
return CLContext()
@pytest.fixture(scope="module")
def modeld(cl_context):
print("Initializing AlpamayoModelD...")
return AlpamayoModelD(cl_context)
@pytest.fixture(scope="function")
def vipc_server():
server_name = "camerad_test"
server = VisionIpcServer(server_name)
server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 1, 1024, 512)
server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 1, 1024, 512)
server.start_listener()
yield server
def test_modeld(cl_context, modeld, vipc_server):
v_ego = 20.0
inputs = {
'input_ids': np.zeros((1, 16), dtype=np.int64),
'history': np.zeros((1, 16, 12), dtype=np.float32),
'logic_pulse': modeld.logic_pulse,
'traffic_convention': np.array([[1.0, 0.0]], dtype=np.float32),
'v_ego': v_ego
}
server_name = "camerad_test"
client_road = VisionIpcClient(server_name, VisionStreamType.VISION_STREAM_ROAD, False, cl_context)
assert client_road.connect(True), "Road client failed to connect"
client_wide = VisionIpcClient(server_name, VisionStreamType.VISION_STREAM_WIDE_ROAD, False, cl_context)
assert client_wide.connect(True), "Wide client failed to connect"
# NV12 size for 1024x512 = 1024*512 * 1.5 = 786432
yuv_data = b'\x00' * 786432
vipc_server.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data)
vipc_server.send(VisionStreamType.VISION_STREAM_WIDE_ROAD, yuv_data)
buf_road = client_road.recv()
buf_wide = client_wide.recv()
assert buf_road is not None
assert buf_wide is not None
bufs = {'road': buf_road, 'wide': buf_wide}
transforms = {'road': np.eye(3, dtype=np.float32), 'wide': np.eye(3, dtype=np.float32)}
outputs = modeld.run(bufs, transforms, inputs, False)
assert outputs is not None
assert outputs["position"].shape == (1, 64, 3)
assert outputs["velocity"].shape == (1, 64, 3)
assert outputs["acceleration"].shape == (1, 64, 3)
assert outputs["orientation"].shape == (1, 64, 3)
assert "pred_pulse" in outputs
assert "pred_light" in outputs
assert "pred_lead" in outputs
assert np.all(np.isfinite(outputs["position"])), "Position contains NaN/Inf"
assert np.all(np.isfinite(outputs["velocity"])), "Velocity contains NaN/Inf"
assert "consistency_error" in outputs
assert outputs["consistency_error"] >= 0.0
model = messaging.new_message('modelV2')
CP = car.CarParams.new_message()
CP.longitudinalActuatorDelay = 0.2
fill_alpamayo_msg(model.modelV2, outputs, 12345, 0.0, 1e9, CP, 0.1, v_ego)
# these just ensure that the model should outputs same action for the same black pixels
assert model.modelV2.action.desiredAcceleration == pytest.approx(-6.75, abs=1e-2)
assert model.modelV2.action.desiredCurvature == pytest.approx(-0.05, abs=1e-2)
assert not model.modelV2.action.shouldStop
assert model.modelV2.frameId == 12345

View File

@@ -60,7 +60,7 @@ def tg_compile(flags, model_name):
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
if File(f"models/{model_name}.onnx").exists():
flags = {
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0',
'larch64': 'DEV=QCOM',
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
tg_compile(flags, model_name)

View File

@@ -1,11 +1,4 @@
#!/usr/bin/env python3
import os
from openpilot.system.hardware import TICI
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
USBGPU = "USBGPU" in os.environ
if USBGPU:
os.environ['DEV'] = 'AMD'
os.environ['AMD_IFACE'] = 'USB'
import time
import numpy as np
import cereal.messaging as messaging
@@ -73,7 +66,7 @@ class ModelState(ModelStateBase):
self.PLANPLUS_CONTROL: float = 1.0
buffer_length = 5 if self.model_runner.is_20hz else 2
self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names}
self.frames = {name: DrivingModelFrame(context, 512, 256, buffer_length) for name in self.model_runner.vision_input_names}
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
# img buffers are managed in openCL transform code

View File

@@ -5,7 +5,11 @@
#include "common/clutil.h"
DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length) : ModelFrame(device_id, context), buffer_length(buffer_length) {
DrivingModelFrame::DrivingModelFrame(cl_device_id device_id, cl_context context, int width, int height, uint8_t buffer_length) : ModelFrame(device_id, context), MODEL_WIDTH(width), MODEL_HEIGHT(height), buffer_length(buffer_length) {
MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2;
buf_size = MODEL_FRAME_SIZE * 2;
frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t);
input_frames = std::make_unique<uint8_t[]>(buf_size);
input_frames_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buf_size, NULL, &err));
img_buffer_20hz_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, buffer_length*frame_size_bytes, NULL, &err));

View File

@@ -64,15 +64,15 @@ protected:
class DrivingModelFrame : public ModelFrame {
public:
DrivingModelFrame(cl_device_id device_id, cl_context context, uint8_t buffer_length);
DrivingModelFrame(cl_device_id device_id, cl_context context, int width, int height, uint8_t buffer_length);
~DrivingModelFrame();
cl_mem* prepare(cl_mem yuv_cl, int frame_width, int frame_height, int frame_stride, int frame_uv_offset, const mat3& projection);
const int MODEL_WIDTH = 512;
const int MODEL_HEIGHT = 256;
const int MODEL_FRAME_SIZE = MODEL_WIDTH * MODEL_HEIGHT * 3 / 2;
const int buf_size = MODEL_FRAME_SIZE * 2;
const size_t frame_size_bytes = MODEL_FRAME_SIZE * sizeof(uint8_t);
int MODEL_WIDTH;
int MODEL_HEIGHT;
int MODEL_FRAME_SIZE;
int buf_size;
size_t frame_size_bytes;
const uint8_t buffer_length;
private:

View File

@@ -20,7 +20,7 @@ cdef extern from "sunnypilot/modeld_v2/models/commonmodel.h":
cppclass DrivingModelFrame:
int buf_size
DrivingModelFrame(cl_device_id, cl_context, unsigned char)
DrivingModelFrame(cl_device_id, cl_context, int, int, unsigned char)
cppclass MonitoringModelFrame:
int buf_size

View File

@@ -59,8 +59,8 @@ cdef class ModelFrame:
cdef class DrivingModelFrame(ModelFrame):
cdef cppDrivingModelFrame * _frame
def __cinit__(self, CLContext context, int buffer_length=2):
self._frame = new cppDrivingModelFrame(context.device_id, context.context, buffer_length)
def __cinit__(self, CLContext context, int width, int height, int buffer_length=2):
self._frame = new cppDrivingModelFrame(context.device_id, context.context, width, height, buffer_length)
self.frame = <cppModelFrame*>(self._frame)
self.buf_size = self._frame.buf_size

View File

@@ -116,7 +116,7 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v14.json"
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v11.json"
def __init__(self, params: Params):
self.params = params

View File

@@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths
from pathlib import Path
# see the README.md for more details on the model selector versioning
CURRENT_SELECTOR_VERSION = 14
REQUIRED_MIN_SELECTOR_VERSION = 14
CURRENT_SELECTOR_VERSION = 13
REQUIRED_MIN_SELECTOR_VERSION = 12
USE_ONNX = os.getenv('USE_ONNX', PC)

View File

@@ -1,7 +1,9 @@
import os
from abc import abstractmethod, ABC
import numpy as np
from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.system.hardware import TICI
from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, CLMemDict, FrameDict, Model, SliceDict, SEND_RAW_PRED
from openpilot.system.hardware.hw import Paths
import pickle
@@ -9,6 +11,15 @@ import pickle
CUSTOM_MODEL_PATH = Paths.model_root()
# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration
USBGPU = "USBGPU" in os.environ
if USBGPU:
os.environ['DEV'] = 'AMD'
os.environ['AMD_IFACE'] = 'USB'
else:
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
class ModelData:
"""
Stores metadata and configuration for a specific machine learning model.

View File

@@ -51,7 +51,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
self.input_to_dtype = {}
self.input_to_device = {}
for idx, name in enumerate(self.model_run.captured.expected_names):
info = self.model_run.captured.expected_input_info[idx]
info = self.model_run.captured.expected_st_vars_dtype_device[idx]
self.input_to_dtype[name] = info[2] # dtype
self.input_to_device[name] = info[3] # device
@@ -84,7 +84,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
def _run_model(self) -> NumpyDict:
"""Runs the Tinygrad model inference and parses the outputs."""
outputs = self.model_run(**self.inputs).numpy().flatten()
outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy()
return self._parse_outputs(outputs)
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:

View File

@@ -17,16 +17,13 @@ class BlinkerPauseLateral:
self.enabled = self.params.get_bool("BlinkerPauseLateralControl")
self.is_metric = self.params.get_bool("IsMetric")
self.min_speed = 0
self.reengage_delay = 0
self.blinker_off_timer = 0.0
def get_params(self) -> None:
self.enabled = self.params.get_bool("BlinkerPauseLateralControl")
self.is_metric = self.params.get_bool("IsMetric")
self.min_speed = self.params.get("BlinkerMinLateralControlSpeed", return_default=True)
self.reengage_delay = self.params.get("BlinkerLateralReengageDelay", return_default=True)
self.min_speed = self.params.get("BlinkerMinLateralControlSpeed")
def update(self, CS: car.CarState, DT_CTRL: float = 0.01) -> bool:
def update(self, CS: car.CarState) -> bool:
if not self.enabled:
return False
@@ -34,11 +31,4 @@ class BlinkerPauseLateral:
speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
min_speed_ms = self.min_speed * speed_factor
below_speed = CS.vEgo < min_speed_ms
if one_blinker and below_speed:
self.blinker_off_timer = self.reengage_delay
elif self.blinker_off_timer > 0:
self.blinker_off_timer -= DT_CTRL
return bool((one_blinker and below_speed) or self.blinker_off_timer > 0)
return bool(one_blinker and CS.vEgo < min_speed_ms)

View File

@@ -5,7 +5,6 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
import pytest
import cereal.messaging as messaging
from cereal import custom, log
@@ -13,52 +12,11 @@ from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision
VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
def _th_above_f32(th: float) -> float:
"""
Return the next representable float32 *above* `th`.
This avoids flaky comparisons around thresholds due to float32 rounding.
"""
th32 = np.float32(th)
above32 = np.nextafter(th32, np.float32(np.inf), dtype=np.float32)
return float(above32)
def _build_single_spike_filtered(n: int, base: float = 1.0) -> np.ndarray:
"""
Create an array where max() is >= threshold but p97 is < threshold.
This demonstrates the behavior difference vs np.amax().
Note: We intentionally construct using float32-representable values to match
the data path through cereal/capnp.
"""
th = float(_ENTERING_PRED_LAT_ACC_TH)
th32 = float(np.float32(th))
# numpy percentile default is linear interpolation: idx=(n-1)*p/100
idx = (n - 1) * 0.97
w = float(idx - np.floor(idx))
base32 = float(np.float32(base))
# Choose spike so that p97 = base + w*(spike-base) < th
# -> spike < base + (th-base)/w. Use a margin (0.9) and ensure spike >= th.
if w == 0.0:
spike = th32 + 1.0
else:
spike = base32 + (th32 - base32) / w * 0.9
spike = max(spike, th32 + 0.01)
arr = np.full(n, base32, dtype=np.float32)
arr[-1] = np.float32(spike)
return arr
def generate_modelV2():
model = messaging.new_message('modelV2')
position = log.XYZTData.new_message()
@@ -143,72 +101,4 @@ class TestSmartCruiseControlVision:
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
assert self.scc_v.state == VisionState.enabled
@pytest.mark.parametrize(
"case, should_enter",
[
("p97_just_above_threshold", True),
("single_spike_filtered", False),
("persistent_high_values", True),
],
ids=[
"p97>threshold_enters",
"single_spike_max_large_but_p97_below_threshold",
"high_values_persist_trigger_entering",
],
)
def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter):
n = len(ModelConstants.T_IDXS)
th = float(_ENTERING_PRED_LAT_ACC_TH)
if case == "p97_just_above_threshold":
# Use the next representable float32 above threshold to avoid float32 rounding flakiness.
val = _th_above_f32(th)
pred_lat_accels = np.full(n, np.float32(val), dtype=np.float32)
elif case == "single_spike_filtered":
pred_lat_accels = _build_single_spike_filtered(n, base=1.0)
elif case == "persistent_high_values":
# Make enough "high" samples so p97 is driven by the persistent trend, not a single outlier.
high_count = max(2, int(np.ceil(n * 0.03)) + 1)
pred_lat_accels = np.full(n, np.float32(1.0), dtype=np.float32)
pred_lat_accels[-high_count:] = np.float32(2.0)
pred_lat_accels[-1] = np.float32(8.0) # keep one big outlier too
else:
raise AssertionError(f"Unknown case: {case}")
# Override model predictions so:
# predicted_lat_accels = abs(orientationRate.z) * velocity.x == pred_lat_accels
mdl = generate_modelV2()
mdl.modelV2.velocity.x = [1.0 for _ in range(n)]
mdl.modelV2.orientationRate.z = [float(x) for x in pred_lat_accels]
self.sm["modelV2"] = mdl.modelV2
v_ego = float(MIN_V + 5.0)
# 1st update: disabled -> enabled
self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0)
# 2nd update: evaluate entering condition from enabled state
self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0)
# Controller does percentile on numpy float64 arrays (values already quantized by capnp),
# so compute expected in float64 to match behavior and avoid interpolation/rounding deltas.
expected_p97 = float(np.percentile(pred_lat_accels.astype(np.float64), 97))
# allow tiny numeric differences due to float conversions/interpolation
assert np.isclose(self.scc_v.max_pred_lat_acc, expected_p97, rtol=1e-6, atol=1e-5)
if should_enter:
# We assert entering primarily by state (this is the actual intended behavior).
assert self.scc_v.state == VisionState.entering
# Optional sanity: should be >= threshold with some margin (since we used nextafter above threshold).
assert self.scc_v.max_pred_lat_acc > th
else:
# Difference vs np.amax(): max can be above threshold, but p97 stays below it.
assert float(np.max(pred_lat_accels)) >= th
assert self.scc_v.max_pred_lat_acc < th
assert self.scc_v.state == VisionState.enabled
# TODO-SP: mock modelV2 data to test other states

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.percentile(predicted_lat_accels, 97)
self.max_pred_lat_acc = np.amax(predicted_lat_accels)
# get the maximum curve based on the current velocity
v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations

Some files were not shown because too many files have changed in this diff Show More