mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-01 11:34:04 +08:00
Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc370e5a19 | |||
| 0cc8d91b69 | |||
| 2d321aeb6c | |||
| 4dd0036141 | |||
| b2d62aa85a | |||
| 6f67fde67c | |||
| 8275858f4e | |||
| fefb631774 | |||
| 6c07d1e338 | |||
| c7b990a330 | |||
| 3710b49280 | |||
| f05eb234c2 | |||
| 2cb6e18f0b | |||
| ccd85013cd | |||
| 33aec5977c | |||
| aee88a2d40 | |||
| 834705eae8 | |||
| a7e7ba591c | |||
| fdd8bbfdf1 | |||
| f04b5d1693 | |||
| 091f640ac3 | |||
| 2878c992b2 | |||
| f169ca0f4c | |||
| 6c4dde8a07 | |||
| e3446423d6 | |||
| fb38c11888 | |||
| b494d5c880 | |||
| 3cda7e5ac9 | |||
| 8a0249abef | |||
| f38c17f4a2 | |||
| 2c1b7d86a0 | |||
| 25252cb91e | |||
| fd14b4b396 | |||
| 4ef9686a91 | |||
| 2f16acaf36 | |||
| e55db51af5 | |||
| 2381fee499 | |||
| 8e6745ff00 | |||
| 5f95832a98 | |||
| 41c6c61755 | |||
| 4ca2636966 | |||
| baa8937660 | |||
| 1c8fc08089 | |||
| 32805df912 | |||
| 253338756f | |||
| 45adc94b9e | |||
| 9f51339aeb | |||
| 1c2cbc4053 | |||
| 001d0fb87b | |||
| 808b527add | |||
| 42c8649906 | |||
| 0dcd1cd82d | |||
| d539b8cdc5 | |||
| 6cfbf1deb8 | |||
| fbb21f702a | |||
| 430272c3ee | |||
| e3ce3aec8f | |||
| b006d1ea1f | |||
| 7f24dc5b2e | |||
| 4f7eb9dfdb | |||
| 4000d23e8f | |||
| c43abae38e | |||
| e8389b0b75 | |||
| 13ca03f11f | |||
| 46dd0df949 | |||
| 805051be92 | |||
| d2b25f1c39 | |||
| 3ab4674ac1 | |||
| f858e68898 | |||
| 568de54a9f | |||
| 6788419227 | |||
| c0455630b7 | |||
| 8afc09d499 | |||
| 2da663894f | |||
| 3ba9503103 | |||
| 7fc969bf14 | |||
| 74139a60ef | |||
| 0830771789 | |||
| c204f0b165 | |||
| df91c501fa | |||
| 85341237ae | |||
| db52c131c2 | |||
| b099ccf37d | |||
| ec0e5fe694 | |||
| 44fba6ef27 | |||
| a8f53df7ca | |||
| 05621af29e | |||
| e140dc5078 | |||
| 1a4da8bc44 | |||
| 3fa8e99c02 | |||
| 48af4915f5 | |||
| 48c9652f77 | |||
| 4429499362 | |||
| 28d04bffb8 | |||
| c058bb8bc2 | |||
| 5d7c249c68 | |||
| cb0ade2c1a | |||
| ff766ad175 | |||
| 89f4cbd1be | |||
| 2ac07aef16 | |||
| 086e7451af | |||
| 0669069d8d | |||
| 0c1b0070f5 | |||
| 1e988f0bdc | |||
| 4efa732259 | |||
| adf0b7158b | |||
| 6dd504cb51 | |||
| e8aeccb03a | |||
| d6b2cfe067 | |||
| 4284d7e4b3 | |||
| ae8d0f0eb7 | |||
| fe9a037417 | |||
| 1448c89e8f | |||
| fff1f5baa7 | |||
| 75b947322f | |||
| 1279c092b1 | |||
| 71b02a7975 | |||
| a2d019c12d | |||
| 28ed48e35e | |||
| 8a0cea2bbf | |||
| cc6a9330d4 | |||
| eb19d343a6 | |||
| 1ad418ca35 | |||
| 678a90f070 | |||
| 8df85fa083 | |||
| 683b4e51a7 | |||
| 29a4fe5d30 | |||
| 9ee201ea28 | |||
| b7435a43bc | |||
| cc181947b6 | |||
| 8984fb8d26 | |||
| 1fd07f1cd5 | |||
| 34625050a9 | |||
| 7267af7411 | |||
| df38093ff1 | |||
| 5c337e297a | |||
| c0c1c0353d | |||
| ac1968a384 | |||
| 89aa8a294e | |||
| 26a3ec8fcd | |||
| 969ce02810 | |||
| 71f121a68b | |||
| 0c1b81741c | |||
| 9ff53d39c2 | |||
| 65a5b48c30 | |||
| 0e76d0c6f6 | |||
| 2764593fff | |||
| 3fc56ef7e4 | |||
| 6dbfce2fc5 | |||
| 0e30087e9a | |||
| 41bc6d6704 | |||
| 6086120108 | |||
| 77b44257a0 | |||
| 139a5302e9 | |||
| 6ddeb91d16 | |||
| 931620ff00 | |||
| b41f3df1fb | |||
| 9d46558823 | |||
| 5be29a9ad0 | |||
| fc3290c1f1 | |||
| 45de4e2929 |
@@ -43,6 +43,7 @@ bin/
|
||||
*.os
|
||||
*.os-*
|
||||
*.so
|
||||
!openpilot/tools/replay/libmedia_mux.so
|
||||
*.a
|
||||
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
|
||||
*.unchunked
|
||||
@@ -135,3 +136,5 @@ openpilot/selfdrive/modeld/models/*.unchunked
|
||||
|
||||
# Jetlink editable-install metadata.
|
||||
/jetlink_repo/jetlink.egg-info/
|
||||
|
||||
# Local Google Play resolver build output (source and lockfile are versioned).
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
## Design Context
|
||||
|
||||
### Users
|
||||
StarPilot owners reviewing recorded drives through Galaxy on phones, over local access or a tunnel.
|
||||
|
||||
### Brand Personality
|
||||
Clear, dependable, intuitive. Playback should inspire confidence through predictable behavior and accurate feedback.
|
||||
|
||||
### Aesthetic Direction
|
||||
Keep Galaxy's existing interface. This review targets playback compatibility and useful loading and error states, with production quality as the bar.
|
||||
|
||||
### Design Principles
|
||||
- Make playback work across supported phones and desktop browsers.
|
||||
- Prefer the smallest complete fix and reuse the existing efficient media pipeline.
|
||||
- Explain failures accurately and give users an actionable recovery path.
|
||||
- Keep controls responsive and avoid unnecessary device work.
|
||||
@@ -17,3 +17,7 @@ The Ford lateral strategies, extended CAN constructors, camera-radar filtering a
|
||||
Relevant lineage includes the controller and integration work in [`db2bdff05`](https://github.com/BluePilotDev/bluepilot/commit/db2bdff05df103d71df62f45c2a3cb5211aba6e6) and [`d0aac605f`](https://github.com/BluePilotDev/bluepilot/commit/d0aac605f99d37e9da205e419f7989c1e9eaa386), extended CAN and safety work in [`8f8d6d15f`](https://github.com/BluePilotDev/bluepilot/commit/8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d), and manual-turn detection in [`97867c1eb`](https://github.com/BluePilotDev/bluepilot/commit/97867c1eb57b7472f6fc3de62f0fef576e5a5497).
|
||||
|
||||
The vehicle strategy and CAN/native owners preserve that attribution while adapting to the current controller interfaces and shared safety limits. Upstream contributors do not maintain or endorse this adaptation. Applicable notices are retained in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
## Optional Ford angle controller
|
||||
|
||||
The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and Ford-only safety hooks are adapted directly from [BluePilot bp-7.0 e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a), principally by Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham, Praeuner and other BluePilot, sunnypilot and comma contributors. Exact source paths and intentionally stricter safety checks are recorded in that folder. Both upstream `LICENSE` and `LICENSE.md` notices are retained. Its layout follows Alan Polk's September 2026 library proposal; this is a StarPilot adapter, not a published BluePilot library release.
|
||||
|
||||
+4
-1
@@ -330,10 +330,13 @@ SConscript([
|
||||
'openpilot/selfdrive/ui/SConscript',
|
||||
])
|
||||
|
||||
# Replay is also the isolated on-device viewer engine.
|
||||
if GetOption('extras') or arch == "comma_arm64":
|
||||
SConscript(['openpilot/tools/replay/SConscript'])
|
||||
|
||||
# Build desktop-only tools
|
||||
if GetOption('extras') and arch != "comma_arm64":
|
||||
SConscript([
|
||||
'openpilot/tools/replay/SConscript',
|
||||
'openpilot/tools/cabana/SConscript',
|
||||
'openpilot/tools/jotpluggler/SConscript',
|
||||
])
|
||||
|
||||
@@ -74,3 +74,13 @@ the exclusion of the other.
|
||||
## BluePilot Ford support
|
||||
|
||||
The Ford lateral strategies, CAN constructors, camera-radar filtering and native checks include work adapted from BluePilot's `bp-7.0` source, through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. See [CREDITS.md](CREDITS.md#ford-support-adapted-from-bluepilot) for contributors and source revisions. The upstream extension-file and `LICENSE.md` notices reproduced above also apply to the Ford extension sources; they are retained without resolving the difference between their published license descriptions.
|
||||
|
||||
## Optional BluePilot angle library adapter
|
||||
|
||||
The angle-only adapter in `opendbc_repo/opendbc/bluepilot_lateral` is sourced directly from BluePilot bp-7.0 commit e1d051d7ba270261b4455068bd68f1a58db15a4a. The complete upstream MIT notice is retained in that folder's `LICENSE`, and the simultaneously published Custom MIT notice is retained verbatim in `LICENSE.md`. The applicable SUNNYPILOT acknowledgments reproduced above are also retained in its `CREDITS.md`. This preserves the published terms without resolving their scope or representing the adapted extension as unambiguously plain MIT.
|
||||
|
||||
The optional, default-disabled angle lane-centering trim additionally derives from
|
||||
BluePilot bp-dev e22afa6be9b881fa784c92ebb316db47728a3d81. Its exact source and
|
||||
contributors are recorded in the library CREDITS.md; the same root/opendbc MIT
|
||||
and Custom MIT notices coexist unchanged at that revision. This adds attribution,
|
||||
not a license-scope decision or permission grant.
|
||||
|
||||
@@ -44,7 +44,6 @@ for arg in "$@"; do
|
||||
esac
|
||||
done
|
||||
|
||||
original_args=("$@")
|
||||
build_helper="${ROOT_DIR}/scripts/laptop_device_build.sh"
|
||||
if [[ -f /AGNOS && "$(uname -s)" == Linux && "$(uname -m)" == aarch64 ]]; then
|
||||
build_helper="${ROOT_DIR}/scripts/native_device_build.sh"
|
||||
@@ -62,6 +61,8 @@ if [[ $# -gt 0 ]]; then
|
||||
fi
|
||||
shortcut_targets=()
|
||||
passthrough_args=()
|
||||
default_args=()
|
||||
jobs_arg=""
|
||||
shortcut_includes_panda=0
|
||||
panda_generated_files=(panda/board/obj/gitversion.h panda/board/obj/version)
|
||||
|
||||
@@ -78,11 +79,26 @@ while [[ $# -gt 0 ]]; do
|
||||
shortcut_targets+=(openpilot/cereal/libcereal.a openpilot/cereal/libsocketmaster.a
|
||||
openpilot/cereal/messaging/bridge openpilot/cereal/services.h)
|
||||
;;
|
||||
-j|--jobs)
|
||||
passthrough_args+=("$1")
|
||||
default_args+=("$1")
|
||||
if [[ $# -gt 1 ]]; then
|
||||
passthrough_args+=("$2")
|
||||
default_args+=("$2")
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [[ "$1" =~ ^[0-9]+$ ]]; then
|
||||
if [[ "$1" =~ ^0+$ ]]; then
|
||||
echo "ERROR: jobs must be positive" >&2
|
||||
exit 2
|
||||
fi
|
||||
jobs_arg="$1"
|
||||
passthrough_args+=("-j$1")
|
||||
else
|
||||
passthrough_args+=("$1")
|
||||
default_args+=("$1")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -128,7 +144,13 @@ fi
|
||||
|
||||
python3 "${ROOT_DIR}/tools/release/stage_mapd_provider.py" --source "${ROOT_DIR}" --require-tracked
|
||||
|
||||
if [[ "${#original_args[@]}" -gt 0 ]]; then
|
||||
exec "${build_helper}" build "${original_args[@]}"
|
||||
if [[ -n "${jobs_arg}" ]]; then
|
||||
if [[ ${default_args[0]+present} ]]; then
|
||||
exec "${build_helper}" build "${jobs_arg}" "${default_args[@]}"
|
||||
fi
|
||||
exec "${build_helper}" build "${jobs_arg}"
|
||||
fi
|
||||
if [[ ${default_args[0]+present} ]]; then
|
||||
exec "${build_helper}" build "${default_args[@]}"
|
||||
fi
|
||||
exec "${build_helper}" build
|
||||
|
||||
Binary file not shown.
+104
-6
@@ -7,6 +7,7 @@ from libcpp.vector cimport vector
|
||||
from libcpp cimport bool
|
||||
from libc cimport errno
|
||||
from libc.string cimport strerror
|
||||
from libc.stdint cimport uint64_t
|
||||
from cython.operator import dereference
|
||||
|
||||
|
||||
@@ -18,6 +19,19 @@ from .ipc cimport Message as cppMessage
|
||||
from .ipc cimport Event as cppEvent, SocketEventHandle as cppSocketEventHandle
|
||||
|
||||
|
||||
cdef extern from *:
|
||||
"""
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
static inline uint64_t msgq_receive_clock_ns() noexcept {
|
||||
struct timespec ts{};
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
|
||||
return uint64_t(ts.tv_sec) * 1000000000ULL + uint64_t(ts.tv_nsec);
|
||||
}
|
||||
"""
|
||||
uint64_t msgq_receive_clock_ns() noexcept nogil
|
||||
|
||||
|
||||
class IpcError(Exception):
|
||||
def __init__(self, endpoint=None):
|
||||
suffix = f"with {endpoint.decode('utf-8')}" if endpoint else ""
|
||||
@@ -161,6 +175,14 @@ cdef class Poller:
|
||||
cdef class SubSocket:
|
||||
cdef cppSubSocket * socket
|
||||
cdef bool is_owner
|
||||
cdef bool receive_timing_enabled
|
||||
cdef uint64_t rt_calls[2]
|
||||
cdef uint64_t rt_messages
|
||||
cdef uint64_t rt_nulls
|
||||
cdef uint64_t rt_invalid
|
||||
cdef uint64_t rt_totals[7]
|
||||
cdef uint64_t rt_slowest[2][8]
|
||||
cdef uint64_t rt_max[2]
|
||||
|
||||
def __cinit__(self):
|
||||
with nogil:
|
||||
@@ -198,20 +220,96 @@ cdef class SubSocket:
|
||||
with nogil:
|
||||
self.socket.setTimeout(timeout)
|
||||
|
||||
cdef void _reset_receive_timing(self):
|
||||
cdef int i, j
|
||||
self.rt_messages = self.rt_nulls = self.rt_invalid = 0
|
||||
for i in range(7):
|
||||
self.rt_totals[i] = 0
|
||||
for i in range(2):
|
||||
self.rt_calls[i] = self.rt_max[i] = 0
|
||||
for j in range(8):
|
||||
self.rt_slowest[i][j] = 0
|
||||
|
||||
def set_receive_timing(self, bool enabled):
|
||||
self.receive_timing_enabled = enabled
|
||||
self._reset_receive_timing()
|
||||
|
||||
def take_receive_timing(self):
|
||||
cdef int i, j
|
||||
report = {
|
||||
'schema_version': 1, 'clock': 'MONOTONIC',
|
||||
'enabled': self.receive_timing_enabled,
|
||||
'blocking_calls': self.rt_calls[0], 'nonblocking_calls': self.rt_calls[1],
|
||||
'messages': self.rt_messages, 'nulls': self.rt_nulls,
|
||||
'invalid_clock_samples': self.rt_invalid,
|
||||
'totals_ns': dict(zip(('release', 'native', 'reacquire', 'copy', 'delete_release', 'delete_native', 'delete_reacquire'),
|
||||
[self.rt_totals[i] for i in range(7)])),
|
||||
'boundary_names': ('before_release', 'native_entry', 'native_return', 'after_reacquire',
|
||||
'before_delete_release', 'delete_entry', 'delete_return', 'after_delete_reacquire'),
|
||||
'slowest_blocking_ns': tuple(self.rt_slowest[0][j] for j in range(8)),
|
||||
'slowest_nonblocking_ns': tuple(self.rt_slowest[1][j] for j in range(8)),
|
||||
}
|
||||
self._reset_receive_timing()
|
||||
return report
|
||||
|
||||
def receive(self, bool non_blocking=False):
|
||||
cdef cppMessage *msg
|
||||
with nogil:
|
||||
msg = self.socket.receive(non_blocking)
|
||||
cdef uint64_t points[8]
|
||||
cdef uint64_t elapsed
|
||||
cdef int i, kind = 1 if non_blocking else 0
|
||||
cdef bool valid = True
|
||||
cdef bool has_message
|
||||
# Disabled path retains the existing native call/copy/delete order.
|
||||
if not self.receive_timing_enabled:
|
||||
with nogil:
|
||||
msg = self.socket.receive(non_blocking)
|
||||
if msg == NULL:
|
||||
return None
|
||||
else:
|
||||
sz = msg.getSize()
|
||||
m = msg.getData()[:sz]
|
||||
with nogil:
|
||||
del msg
|
||||
return m
|
||||
|
||||
if msg == NULL:
|
||||
return None
|
||||
points[0] = msgq_receive_clock_ns()
|
||||
with nogil:
|
||||
points[1] = msgq_receive_clock_ns()
|
||||
msg = self.socket.receive(non_blocking)
|
||||
points[2] = msgq_receive_clock_ns()
|
||||
points[3] = msgq_receive_clock_ns()
|
||||
has_message = msg != NULL
|
||||
if not has_message:
|
||||
self.rt_nulls += 1
|
||||
for i in range(4, 8):
|
||||
points[i] = 0
|
||||
else:
|
||||
self.rt_messages += 1
|
||||
sz = msg.getSize()
|
||||
m = msg.getData()[:sz]
|
||||
points[4] = msgq_receive_clock_ns()
|
||||
with nogil:
|
||||
points[5] = msgq_receive_clock_ns()
|
||||
del msg
|
||||
|
||||
return m
|
||||
points[6] = msgq_receive_clock_ns()
|
||||
points[7] = msgq_receive_clock_ns()
|
||||
self.rt_calls[kind] += 1
|
||||
for i in range(8 if has_message else 4):
|
||||
if points[i] == 0 or (i > 0 and points[i] < points[i-1]):
|
||||
valid = False
|
||||
if not valid:
|
||||
self.rt_invalid += 1
|
||||
else:
|
||||
for i in range(7 if has_message else 3):
|
||||
self.rt_totals[i] += points[i+1] - points[i]
|
||||
elapsed = points[7 if has_message else 3] - points[0]
|
||||
if elapsed > self.rt_max[kind]:
|
||||
self.rt_max[kind] = elapsed
|
||||
for i in range(8):
|
||||
self.rt_slowest[kind][i] = points[i]
|
||||
if not has_message:
|
||||
return None
|
||||
return m
|
||||
|
||||
|
||||
cdef class PubSocket:
|
||||
|
||||
Binary file not shown.
@@ -443,6 +443,8 @@ int msgq_poll(msgq_pollitem_t * items, size_t nitems, int timeout){
|
||||
if (items[i].revents) num++;
|
||||
}
|
||||
|
||||
if (timeout == 0) return num;
|
||||
|
||||
int ms = (timeout == -1) ? 100 : timeout;
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include "msgq/test_runner.h"
|
||||
#include "msgq/msgq.h"
|
||||
@@ -20,6 +22,89 @@ TEST_CASE("ALIGN")
|
||||
REQUIRE(ALIGN(99999) == 100000);
|
||||
}
|
||||
|
||||
TEST_CASE("msgq_poll nonblocking readiness")
|
||||
{
|
||||
cleanup_test_queue();
|
||||
msgq_queue_t writer, first, second;
|
||||
msgq_new_queue(&writer, "test_queue", 1024);
|
||||
msgq_init_publisher(&writer);
|
||||
msgq_new_queue(&first, "test_queue", 1024);
|
||||
msgq_init_subscriber(&first);
|
||||
msgq_new_queue(&second, "test_queue", 1024);
|
||||
msgq_init_subscriber(&second);
|
||||
|
||||
msgq_pollitem_t items[] = {{&first, 1}, {&second, 1}};
|
||||
REQUIRE(msgq_poll(nullptr, 0, 0) == 0);
|
||||
REQUIRE(msgq_poll(items, 2, 0) == 0);
|
||||
REQUIRE(items[0].revents == 0);
|
||||
REQUIRE(items[1].revents == 0);
|
||||
|
||||
char data[] = "poll payload";
|
||||
msgq_msg_t outgoing, incoming;
|
||||
msgq_msg_init_data(&outgoing, data, sizeof(data));
|
||||
REQUIRE(msgq_msg_send(&outgoing, &writer) == sizeof(data));
|
||||
REQUIRE(msgq_poll(items, 2, 0) == 2);
|
||||
REQUIRE(items[0].revents == 1);
|
||||
REQUIRE(items[1].revents == 1);
|
||||
REQUIRE(msgq_msg_recv(&incoming, &first) == sizeof(data));
|
||||
REQUIRE(memcmp(incoming.data, data, sizeof(data)) == 0);
|
||||
msgq_msg_close(&incoming);
|
||||
|
||||
REQUIRE(msgq_poll(items, 2, 0) == 1);
|
||||
REQUIRE(items[0].revents == 0);
|
||||
REQUIRE(items[1].revents == 1);
|
||||
REQUIRE(msgq_msg_recv(&incoming, &second) == sizeof(data));
|
||||
REQUIRE(memcmp(incoming.data, data, sizeof(data)) == 0);
|
||||
msgq_msg_close(&incoming);
|
||||
REQUIRE(msgq_poll(items, 2, 0) == 0);
|
||||
REQUIRE(items[0].revents == 0);
|
||||
REQUIRE(items[1].revents == 0);
|
||||
|
||||
msgq_msg_close(&outgoing);
|
||||
msgq_close_queue(&second);
|
||||
msgq_close_queue(&first);
|
||||
msgq_close_queue(&writer);
|
||||
cleanup_test_queue();
|
||||
}
|
||||
|
||||
TEST_CASE("msgq_poll waiting modes")
|
||||
{
|
||||
for (int timeout : {1000, -1}) {
|
||||
cleanup_test_queue();
|
||||
msgq_queue_t writer, reader;
|
||||
msgq_new_queue(&writer, "test_queue", 1024);
|
||||
msgq_init_publisher(&writer);
|
||||
msgq_new_queue(&reader, "test_queue", 1024);
|
||||
msgq_init_subscriber(&reader);
|
||||
msgq_pollitem_t item{&reader, 1};
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
REQUIRE(msgq_poll(&item, 1, 2) == 0);
|
||||
REQUIRE(item.revents == 0);
|
||||
REQUIRE(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(2));
|
||||
|
||||
msgq_msg_t outgoing;
|
||||
msgq_msg_init_size(&outgoing, 1);
|
||||
outgoing.data[0] = 'p';
|
||||
std::thread sender([&]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
msgq_msg_send(&outgoing, &writer);
|
||||
});
|
||||
int received = msgq_poll(&item, 1, timeout);
|
||||
sender.join();
|
||||
REQUIRE(received == 1);
|
||||
REQUIRE(item.revents == 1);
|
||||
msgq_msg_t incoming;
|
||||
REQUIRE(msgq_msg_recv(&incoming, &reader) == 1);
|
||||
REQUIRE(incoming.data[0] == 'p');
|
||||
msgq_msg_close(&incoming);
|
||||
msgq_msg_close(&outgoing);
|
||||
msgq_close_queue(&reader);
|
||||
msgq_close_queue(&writer);
|
||||
cleanup_test_queue();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("msgq_msg_init_size")
|
||||
{
|
||||
const size_t msg_size = 30;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import uuid
|
||||
import msgq
|
||||
import pytest
|
||||
|
||||
|
||||
def sockets(*, timeout=5, conflate=False):
|
||||
endpoint = "receive_timing_" + uuid.uuid4().hex
|
||||
pub = msgq.pub_sock(endpoint)
|
||||
sub = msgq.sub_sock(endpoint, timeout=timeout, conflate=conflate)
|
||||
return pub, sub
|
||||
|
||||
|
||||
def check(report):
|
||||
assert report["schema_version"] == 1 and report["clock"] == "MONOTONIC"
|
||||
assert report["blocking_calls"] + report["nonblocking_calls"] == report["messages"] + report["nulls"]
|
||||
assert report["invalid_clock_samples"] == 0
|
||||
assert all(v >= 0 for v in report["totals_ns"].values())
|
||||
for key in ["slowest_blocking_ns", "slowest_nonblocking_ns"]:
|
||||
points = report[key]
|
||||
assert len(points) == 8
|
||||
nonzero = [p for p in points if p]
|
||||
assert nonzero == sorted(nonzero)
|
||||
|
||||
|
||||
def test_disabled_then_enabled_data_and_reset():
|
||||
pub, sub = sockets()
|
||||
pub.send(b"old")
|
||||
assert sub.receive() == b"old"
|
||||
report = sub.take_receive_timing()
|
||||
assert not report["enabled"] and report["messages"] == 0
|
||||
sub.set_receive_timing(True)
|
||||
pub.send(b"new")
|
||||
assert sub.receive() == b"new"
|
||||
report = sub.take_receive_timing()
|
||||
check(report)
|
||||
assert report["enabled"] and report["blocking_calls"] == 1 and report["messages"] == 1
|
||||
assert report["slowest_blocking_ns"][4] > 0
|
||||
empty = sub.take_receive_timing()
|
||||
check(empty)
|
||||
assert empty["messages"] == 0 and empty["slowest_blocking_ns"] == (0,) * 8
|
||||
assert report["messages"] == 1 # detached after reset
|
||||
sub.set_receive_timing(False)
|
||||
pub.send(b"off")
|
||||
assert sub.receive() == b"off"
|
||||
assert sub.take_receive_timing()["messages"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("timeout", [0, 5])
|
||||
def test_empty_timeout_and_nonblocking(timeout):
|
||||
pub, sub = sockets(timeout=timeout)
|
||||
sub.set_receive_timing(True)
|
||||
assert sub.receive() is None
|
||||
assert sub.receive(non_blocking=True) is None
|
||||
report = sub.take_receive_timing()
|
||||
check(report)
|
||||
assert report["blocking_calls"] == report["nonblocking_calls"] == 1
|
||||
assert report["nulls"] == 2 and report["messages"] == 0
|
||||
assert report["slowest_blocking_ns"][4:] == (0,) * 4
|
||||
assert report["slowest_nonblocking_ns"][4:] == (0,) * 4
|
||||
assert report["totals_ns"]["copy"] == report["totals_ns"]["delete_native"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("conflate", [False, True])
|
||||
def test_burst_no_diagnostic_cap_or_drop(conflate):
|
||||
pub, sub = sockets(conflate=conflate)
|
||||
sub.set_receive_timing(True)
|
||||
expected = [str(i).encode() for i in range(80)]
|
||||
for message in expected:
|
||||
pub.send(message)
|
||||
actual = msgq.drain_sock_raw(sub, wait_for_one=True)
|
||||
assert actual == ([expected[-1]] if conflate else expected)
|
||||
report = sub.take_receive_timing()
|
||||
check(report)
|
||||
assert report["messages"] == len(actual)
|
||||
assert report["blocking_calls"] == 1
|
||||
assert report["nonblocking_calls"] == len(actual) # includes terminal null
|
||||
assert report["nulls"] == 1
|
||||
assert report["slowest_nonblocking_ns"][0] > 0
|
||||
assert len(report["slowest_nonblocking_ns"]) == 8
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
<!--- AUTOGENERATED FROM selfdrive/car/CARS_template.md, DO NOT EDIT. --->
|
||||
|
||||
# Support Information for 406 Known Cars
|
||||
# Support Information for 448 Known Cars
|
||||
|
||||
|Make|Model|Package|Support Level|
|
||||
|---|---|---|:---:|
|
||||
@@ -34,12 +34,50 @@
|
||||
|Audi|Q5 2017-24|All|[Not compatible](#flexray)|
|
||||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|
||||
|Buick|Baby Enclave (SDGM Harness) 2020-23|Driver Assist Package|[Community](#community)|
|
||||
|Buick|LaCrosse (ASCM Harness) 2017-19|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Buick|LaCrosse (OBD-II Harness) 2017-19|Driver Confidence Package 2|[Community](#community)|
|
||||
|Buick|LaCrosse US (ASCM Harness) 2019|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Buick|Regal Essence (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|BYD|Atto 3 2022-25|All|[Dashcam mode](#dashcam)|
|
||||
|Cadillac|ATS Premium Performance (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Cadillac|CT6 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Cadillac|Escalade (ASCM Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Cadillac|Escalade (OBD-II Harness) 2017|Driver Assist Package|[Community](#community)|
|
||||
|Cadillac|Escalade ESV (OBD-II Harness) 2016|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|
||||
|Cadillac|Escalade ESV (OBD-II Harness) 2019|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|
||||
|Cadillac|Escalade ESV Platinum (ASCM Harness) 2019|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Cadillac|XT4 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Cadillac|XT4 (SDGM Harness) 2023|Driver Assist Package|[Community](#community)|
|
||||
|Cadillac|XT5 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Cadillac|XT5 (SDGM Harness) 2022|Driver Assist Package|[Community](#community)|
|
||||
|Cadillac|XT6 (SDGM Harness) 2020|Driver Assist Package|[Community](#community)|
|
||||
|Chevrolet|Blazer (SDGM Harness) 2019-25|Driver Assist Package|[Community](#community)|
|
||||
|Chevrolet|Bolt (Camera Harness) 2022-23|ACC; recognized camera layout required|[Community](#community)|
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|[Upstream](#upstream)|
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|[Upstream](#upstream)|
|
||||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|
||||
|Chevrolet|Equinox (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Chevrolet|Malibu (ASCM Harness) 2017-19|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Chevrolet|Malibu (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Chevrolet|Malibu (SDGM Harness) 2019|SDGM Harness|[Community](#community)|
|
||||
|Chevrolet|Malibu Hybrid (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Chevrolet|Malibu Premier (OBD-II Harness) 2017|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|[Upstream](#upstream)|
|
||||
|Chevrolet|Silverado (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Chevrolet|Suburban (Camera Harness)|Manual camera configuration; recognized CAN layout required|[Community](#community)|
|
||||
|Chevrolet|Suburban (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Chevrolet|Suburban (OBD-II Harness)|Configuration-specific install; observed CAN sources required|[Community](#community)|
|
||||
|Chevrolet|Suburban Premier (ASCM Harness) 2016-20|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|
||||
|Chevrolet|Trailblazer (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Chevrolet|Traverse (SDGM Harness) 2022-23|RS, Premier, or High Country Trim|[Community](#community)|
|
||||
|Chevrolet|Trax (Camera Harness)|Manual camera configuration; recognized CAN layout required|[Community](#community)|
|
||||
|Chevrolet|Volt (ASCM Harness) 2017-18|Adaptive Cruise Control (ACC); custom ASCM harness install|[Community](#community)|
|
||||
|Chevrolet|Volt (Camera Harness)|Manual configuration; recognized camera layout required|[Community](#community)|
|
||||
|Chevrolet|Volt (CC)|Manual development configuration; installation must be verified|[Community](#community)|
|
||||
|Chevrolet|Volt (OBD-II Harness) 2017-18|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Chevrolet|Volt (SDGM Harness) 2019|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|
||||
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|
||||
|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|
||||
|Chrysler|Pacifica 2021-23|All|[Upstream](#upstream)|
|
||||
@@ -88,8 +126,12 @@
|
||||
|Genesis|GV70 Electrified (Australia Only) 2022|All|[Upstream](#upstream)|
|
||||
|Genesis|GV70 Electrified (with HDA II) 2023-24|Highway Driving Assist II|[Upstream](#upstream)|
|
||||
|Genesis|GV80 2023|All|[Upstream](#upstream)|
|
||||
|GMC|Acadia (ASCM Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|GMC|Acadia (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|[Upstream](#upstream)|
|
||||
|GMC|Yukon 2019-20|Adaptive Cruise Control (ACC) & LKAS|[Dashcam mode](#dashcam)|
|
||||
|GMC|Yukon (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|
||||
|Holden|Astra (OBD-II Harness) 2017|Adaptive Cruise Control (ACC)|[Community](#community)|
|
||||
|Honda|Accord 2016-17|Honda Sensing|[Community](#community)|
|
||||
|Honda|Accord 2018-22|All|[Upstream](#upstream)|
|
||||
|Honda|Accord 2023-25|All|[Upstream](#upstream)|
|
||||
@@ -473,4 +515,4 @@ Toyota, and the GM Global B platform.
|
||||
All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wiki/CAN_bus) for communication between all the car's computers, however a
|
||||
CAN bus isn't the only way that the computers in your car can communicate. Most, if not all, vehicles from the following
|
||||
manufacturers use [FlexRay](https://en.wikipedia.org/wiki/FlexRay) instead of a CAN bus: **BMW, Mercedes, Audi, Land Rover, and some Volvo**. These cars
|
||||
may one day be supported, but we have no immediate plans to support FlexRay.
|
||||
may one day be supported, but we have no immediate plans to support FlexRay.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# BluePilot angle source
|
||||
|
||||
Adapted from BluePilotDev/bluepilot bp-7.0 at
|
||||
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a),
|
||||
principally Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham,
|
||||
Praeuner, tonesto7, Haibin Wen and other BluePilot, sunnypilot and comma contributors.
|
||||
Source paths: opendbc/sunnypilot/car/ford/lateral_angle_ext.py,
|
||||
fordcan_ext.py, human_turn.py and opendbc/safety/modes/ford.h.
|
||||
The simultaneous upstream LICENSE and LICENSE.md notices are retained.
|
||||
This software is licensed under a custom license requiring permission for use.
|
||||
This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
|
||||
|
||||
This is a StarPilot adapter to the September 2026 proposed library shape.
|
||||
It is not a released BluePilot library, tag, or claim of BluePilot validation.
|
||||
The existing curvature controller remains StarPilot-owned. Pinion curvature,
|
||||
curvature-primary settings and the unsafe native reset bypass are not imported.
|
||||
|
||||
The exact upstream root notices are additionally retained as upstream-root-LICENSE
|
||||
and upstream-root-LICENSE.md; LICENSE/ LICENSE.md retain the opendbc notices.
|
||||
ORIGIN_HISTORY.tsv records the inspected path history with commit IDs/authors/dates.
|
||||
The private compact upstream root clone preserves its reachable root history.
|
||||
Pre-conversion opendbc submodule objects are not included in that root history.
|
||||
|
||||
Angle lane-centering trim is separately adapted from BluePilot bp-dev
|
||||
[e22afa6be9b881fa784c92ebb316db47728a3d81](https://github.com/BluePilotDev/bluepilot/commit/e22afa6be9b881fa784c92ebb316db47728a3d81),
|
||||
`opendbc/sunnypilot/car/ford/lane_center_trim.py` (blob
|
||||
`5eedebd79e99493624f9d6af0750d1dd5713306c`). Alan Polk authored the initial
|
||||
trim, model-position fallback, correction slew and default-strength changes;
|
||||
ghbarker contributed reference/NaN guards and tests. The source explicitly credits
|
||||
StarPilot/u/jc01rho for curvature-domain placement/geometry. The core adaptation
|
||||
adds immutable host projection and conservative stale/driver-pressure suppression.
|
||||
The pinned bp-7.0 controller baseline and default-disabled policy are unchanged.
|
||||
Both simultaneous root/opendbc MIT and Custom MIT notices also exist unchanged at
|
||||
this trim revision; all retained notices and permission acknowledgments apply
|
||||
without assigning an exclusive license or claiming a new permission grant.
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2020, Comma.ai, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Custom MIT License
|
||||
|
||||
Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
|
||||
|
||||
1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
|
||||
|
||||
2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
|
||||
"This software is licensed under a custom license requiring permission for use."
|
||||
|
||||
3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
|
||||
"This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
|
||||
|
||||
4. **No Warranty**: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
|
||||
|
||||
---
|
||||
|
||||
Haibin Wen, SUNNYPILOT LLC
|
||||
@@ -0,0 +1,38 @@
|
||||
9d190417bf74563abfa6f36996f1e673e9ee0a1b Praeuner 2026-08-13T22:26:15-05:00 Update dampening factor to .25
|
||||
a881646ea5edc21c0db4c4e74076e0dc14671a4e alan-polk 2026-08-10T09:04:52-05:00 Merge pull request #176 from BluePilotDev/bp-no-stall
|
||||
9012f76666a5c90764fcaec40832b9a607488c27 John Christman 2026-08-05T17:49:59-04:00 anti-stall
|
||||
3610e3f18a0ad37d793d7ada61dbc739ab1077a3 Nathan Ingraham 2026-07-23T22:26:29-05:00 self.path_angle_gain_lowC_highV remains the platform-specific base gain. The toggle now controls separate self.user_dampening_factor. l ow_gain_calc uses: self.path_angle_gain_lowC_highV * self.user_dampening_factor UI placement, description, default, and 0.75–1.25 range remain unchanged.
|
||||
1db52bc79e607ddb00214254dd4d732615e27fe4 Nathan Ingraham 2026-07-23T00:00:51-05:00 -Added “High Speed Dampening” directly below “High Speed Adjustment Factor”, range 0.75–1.25, step 0.01, and default 1.0.
|
||||
5d994c24edf63f3b9004b2963f18ef4199e5d368 Jacob Neulight 2026-07-18T20:17:04-06:00 Ford safety: optional pinion-sourced angle_meas behind STEER_ANGLE_CURVATURE SP flag
|
||||
699c17d9fde62c690eed9d68eed7d731744d5137 Jacob Neulight 2026-07-18T19:53:46-06:00 Ford: publish angle-mode shadow curvature from measured curvature during overrides
|
||||
b7428fee4b116d4832f91c850d54cac2730de2ef alan-polk 2026-07-18T08:52:24-05:00 restore curvature safety checks to bp-6.0 logic.
|
||||
97867c1eb57b7472f6fc3de62f0fef576e5a5497 alan-polk 2026-07-16T19:18:05-05:00 Human Turn Reset for Angle (#118)
|
||||
7efee67e3e643d52b973895d6c6765c5cc12e314 alan-polk 2026-07-16T08:19:43-05:00 angle mode: rescale exit-detection threshold for 20Hz cadence
|
||||
1958a3501ead10ae1f496f0d11e1e8cda6e2dbcc alan-polk 2026-07-14T15:22:38-05:00 Bp 7.0 variable cleanup (#111)
|
||||
8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d alan-polk 2026-07-14T08:53:28-05:00 Bp 7.0 panda2 (#109)
|
||||
11ac3b760671b489ca449d522f1edf240a232884 alan-polk 2026-07-09T07:53:21-05:00 angle mode: fix ROC values tuned for 100Hz cadence running at actual 20Hz
|
||||
dbdcea747e65f574d326a55c9bf1a9d6c9485e22 alan-polk 2026-07-08T09:21:32-05:00 Revert "ford: add current-curvature deviation check to angle mode (panda-side)"
|
||||
69a9714563622729ba304ac75354ba3fd37b0b39 alan-polk 2026-07-08T09:17:09-05:00 ford: add current-curvature deviation check to angle mode (panda-side)
|
||||
be7f86a5d0dc1ea3b3f0052d4b54ff0b8a70f843 John Christman 2026-07-06T21:53:55-04:00 cast on debug
|
||||
5c1fbb07dd4d7d950a01cb2cd1faddd90d41e632 alan-polk 2026-07-06T14:20:49-05:00 controllerStateBP: add angleRateLimited + curvatureRateLimited diagnostics
|
||||
06d654e5f48ea8fcffce6e98e48e3a30450b5097 alan-polk 2026-06-25T23:28:51-05:00 Revert "temp blend ratio change" — restore predicted-curvature blend to 0.50
|
||||
540e69fcf4861a2ce5d7ca41467effc231638e27 alan-polk 2026-06-22T08:35:44-05:00 temp blend ratio change
|
||||
e3819c7e630d85cfb44858840124fe7f72f1762d alan-polk 2026-06-13T20:47:09-05:00 ford.h: don't block angle mode on the inactive curvature signal
|
||||
5d343738e30267688d0ffa6eebe2ec3ca57b10ee alan-polk 2026-06-13T18:28:30-05:00 ford.h: widen path_angle ROC to 2% looser than lateral_angle_ext _soft_roc
|
||||
d0aac605f99d37e9da205e419f7989c1e9eaa386 alan-polk 2026-06-13T11:15:50-05:00 BluePilot: add angle-primary lateral control mode
|
||||
51d5de8991508c862e7c51333e5055425b2b8d23 alan-polk 2026-04-11T12:00:10-05:00 fixing missing punctation in ford.h
|
||||
8194ceb7e0bbc112c73d393a8f0add3261f6592d alan-polk 2026-04-11T11:51:49-05:00 make safety limits symetrical but control limits asymetrical. Add 1.5s duration to detect human turn
|
||||
db2bdff05df103d71df62f45c2a3cb5211aba6e6 alan-polk 2026-04-09T23:47:30-05:00 move all external logic to lateral_curv_ext.py. Apply some order of operations fixes to try and eleminate panda blocking messages.
|
||||
1a8ac23b5bd5d7952c15eb913c2b1e39f14ad92e Blue Genie 2026-04-08T15:10:24-05:00 Adjust panda limits
|
||||
892be076daf898d806983f847c72357accff7312 Blue Genie 2026-04-08T11:45:28-05:00 Add logging functionality
|
||||
ce7d27230657159332f64a253a0faea6cf8dcc95 alan-polk 2026-04-08T11:40:02-05:00 fix rate of change limits
|
||||
021b5146e80ac58fe2e81dbd848bc7cd409ef08e alan-polk 2026-04-07T16:24:38-05:00 align ford.h limits with dbc and carcontroller
|
||||
7f9212f7f8dc22c4a0a22443158554f4d3652a3c tonesto7 2026-04-05T21:28:37-04:00 Merge bp-6.1 upstream changes into bp-6.0-dev (#124)
|
||||
dd1a9d3fd8f60462d3d8ac6de3e7860e70e1f667 alan-polk 2025-12-12T12:22:31-06:00 convert opendbc from a submodule to a subfolder
|
||||
10101012d8a4cc37951a557023824cdd995443a6 alan-polk 2026-08-10T17:23:04Z BluePilot: angle-mode lane centering trim (advanced lane positioning)
|
||||
560e80e9d593badca08aa60e8ddb692e58033425 alan-polk 2026-08-11T12:50:00Z BluePilot: angle-mode lane centering falls back to model position, not zero
|
||||
94bf8144d4a683e9f3408f3babf892878f3b81f4 ghbarker 2026-08-12T01:31:33Z ford ALP: restore StarPilot guards (narrow lane, laneline std, NaN params) and cap applied correction at 0.0015
|
||||
80825b81568ad684fe4ec37a797d3cbaa004fdcf ghbarker 2026-08-13T01:04:02Z split applied-correction ceiling into its own PR per review
|
||||
e5b92cc48b34ea43ac756a96a96687c9851a188d ghbarker 2026-08-13T01:05:07Z add tests for the restored guards
|
||||
af4bc410c9ef417ba723212ceaa2a77758dd8ecb alan-polk 2026-08-14T03:24:28Z BluePilot: rate-limit the lane centering correction, not just filter it
|
||||
8eea9eb68e0c4126ee1c52f251e274d5865b6743 alan-polk 2026-08-19T13:33:00Z BluePilot: lower angle-mode lane centering default strength to 0.25
|
||||
|
Can't render this file because it contains an unexpected character in line 14 and column 85.
|
@@ -0,0 +1,81 @@
|
||||
# BluePilot Ford angle control
|
||||
|
||||
StarPilot adaptation of BluePilotDev/bluepilot `bp-7.0`, pinned to
|
||||
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a).
|
||||
The layout follows Alan Polk's September 2026 library proposal. This is an
|
||||
adaptation, not an upstream library release.
|
||||
|
||||
Existing curvature control remains the default. Explicit `FordLateralMode=1`
|
||||
selects angle control next drive; the saved choice persists when unavailable.
|
||||
Only this selector is exposed. Fixed defaults and bounds live in
|
||||
`params/manifest.json`; pinion-yaw correction and curvature-primary tuning are
|
||||
outside this library. Angle lane-centering trim is available as a default-disabled
|
||||
core capability, separately sourced from BluePilot bp-dev
|
||||
`e22afa6be9b881fa784c92ebb316db47728a3d81`; it adds no public settings.
|
||||
|
||||
`core/` accepts scalar/model inputs without openpilot, Params or messaging
|
||||
imports. `can/` owns encoding, `safety/ford_bp.h` owns native enforcement, and
|
||||
`hosts/starpilot.py` connects the host. `params/protocol.json` generates the wire
|
||||
constants. The folder is vendored without submodules, LFS or an external runtime.
|
||||
|
||||
## Admission
|
||||
|
||||
Classic: Bronco Sport Mk1, Escape Mk4, Focus Mk4, Maverick Mk1, Explorer Mk6.
|
||||
CAN FD: Escape Mk4.5, Expedition Mk4, F150 Mk14, F150 Lightning Mk1, Ranger Mk2,
|
||||
Mustang Mach-E Mk1.
|
||||
|
||||
Exact classic safety parameters 32/33 map to angle 128/129; CAN FD 18/19 and
|
||||
66/67 map to 130/131. Parameter 131 requires DEBUG; RELEASE retains stock 130.
|
||||
Longitudinal, radar, fingerprint and parser policy remain host-owned. Admission
|
||||
uses the final CarParams contract, not brand or steering-control enum alone.
|
||||
|
||||
AOL, alternative experiences, Transit, Edge, Mondeo, SecOC, passive, dashcam,
|
||||
notCar and unsupported static flags are excluded. Auxiliary noOutput requires
|
||||
parameter zero; production deployments with two Pandas remain unqualified until
|
||||
actual startup, bus routing and public acknowledgments are verified.
|
||||
|
||||
## Safety design
|
||||
|
||||
The extension uses small Ford hooks without changing shared safety headers or
|
||||
existing curvature namespaces. Active steering requires ordinary controls
|
||||
permission, healthy physical RX and a fresh matching public Panda acknowledgment
|
||||
of safety parameter, alternative experience, controls, RX validity, heartbeat and
|
||||
faults. Cruise, gas/brake, checksum, counter, quality and freshness checks apply.
|
||||
Neutral mode cannot grant permission.
|
||||
|
||||
The BluePilot reset-bypass latch is omitted. Rejected frames preserve accepted
|
||||
actuator history and withdraw the shadow lease; accepted neutral commands or
|
||||
permission withdrawal reset history. Lost counters recover through neutral
|
||||
resynchronization. Version or checksum failure cannot fall through to curvature
|
||||
control.
|
||||
|
||||
Active frames carry neutral curvature, offset and curvature rate, with checked
|
||||
mode, ramp, precision, path angle, counter, checksum and cadence. Native enforcement
|
||||
checks the shadow against the actual path across gains 0.95–1.3, bounds both by
|
||||
`min(0.02, 2.4114/v²)`, and bounds the full actual-curvature interval against
|
||||
measured curvature above 10 m/s. Actual path also obeys the 3.5886 m/s³ equivalent
|
||||
jerk limit and rolling message-rate limit. Core quantization and timing margins
|
||||
keep packed commands inside these bounds; an impossible intersection is neutral.
|
||||
|
||||
The versioned 0x3CA announcement preserves StarPilot's existing byte 4 bit 1
|
||||
curvature flag and requires it clear in angle mode. It carries a counter, signed
|
||||
shadow curvature, protocol version and checksum; its lease expires after 60 ms.
|
||||
The shadow derives from the final path after rate limiting and saturation.
|
||||
|
||||
Control computes at 20 Hz, announces through 33 Hz LKA, then sends the latest
|
||||
announced result at 20 Hz, adding at most one steering tick. Permission withdrawal
|
||||
sends neutral immediately. Human-turn recovery uses bounded neutral blips without
|
||||
a native bypass. The retained Dom factory steering delay is 0.22 s; pinned
|
||||
BluePilot uses 0.20 s.
|
||||
|
||||
These stricter native limits and versioned metadata intentionally differ from
|
||||
BluePilot 7.0. Identical bytes or safety decisions are not claimed for affected
|
||||
commands. Runtime and physical ECU qualification remain pending.
|
||||
|
||||
## Attribution and notices
|
||||
|
||||
See `CREDITS.md`, `ORIGIN_HISTORY.tsv` and the project `THIRD_PARTY_NOTICES.md`.
|
||||
Both upstream MIT and Custom MIT notices are retained verbatim in `LICENSE`,
|
||||
`LICENSE.md`, `upstream-root-LICENSE` and `upstream-root-LICENSE.md`. The maintainer
|
||||
reports author permission to use this adaptation under MIT. Both published notices
|
||||
remain intact.
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Adapter to the proposed BluePilot library; angle only, exact source pinned."""
|
||||
from opendbc.bluepilot_lateral.can.ford import lateral, lka
|
||||
from opendbc.bluepilot_lateral.core.angle import Command
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import Host, qualified
|
||||
|
||||
|
||||
class BluePilotLateral:
|
||||
def __init__(self, CP, *, host=None):
|
||||
if not qualified(CP):
|
||||
raise ValueError("Angle mode is not configured")
|
||||
self.host = host or Host(CP)
|
||||
self.canfd = bool(CP.flags & 1)
|
||||
self.pending = Command()
|
||||
self.announced = Command()
|
||||
self.announced_ns = 0
|
||||
self.lka_counter = 0
|
||||
self.apply_curvature_last = 0.0
|
||||
|
||||
def step_lateral(self, CC, CS, packer, CAN, frame, now_ns):
|
||||
self.pending = self.host.update(CC, CS)
|
||||
command = self.announced
|
||||
# A pending permission withdrawal is immediate; an increase waits until
|
||||
# the 33Hz side channel has announced the matching final actuator.
|
||||
if not self.pending.active or not 0 < self.announced_ns <= now_ns <= self.announced_ns + 60_000_000:
|
||||
command = Command()
|
||||
self.announced = command
|
||||
return [lateral(packer, CAN.main, command, canfd=self.canfd, counter=frame // 5)]
|
||||
|
||||
def step_lka(self, packer, CAN, now_ns):
|
||||
self.announced = self.pending
|
||||
self.announced_ns = now_ns
|
||||
message = lka(packer, CAN.main, self.announced.shadow_curvature, self.lka_counter)
|
||||
self.lka_counter = (self.lka_counter + 1) % 8
|
||||
return [message]
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Ford angle codecs, adapted from BluePilot fordcan_ext.py at e1d051d7.
|
||||
|
||||
Core commands use planner signs; path and shadow are negated on the wire.
|
||||
The v1 side channel retains byte4 bit1 for StarPilot's curvature announcement.
|
||||
"""
|
||||
from opendbc.bluepilot_lateral.params import protocol as p
|
||||
|
||||
|
||||
def checksum4(dat):
|
||||
return (15 - (sum(dat[:7]) & 15)) & 15
|
||||
|
||||
|
||||
def lka(packer, bus, shadow, counter):
|
||||
addr, original, _ = packer.make_can_msg("Lane_Assist_Data1", bus, {"LaRefAng_No_Req": 0., "LaCurvature_No_Calc": 0.})
|
||||
dat = bytearray(original)
|
||||
raw = max(-20000, min(20000, round(-shadow / p.SHADOW_SCALE))) & 0xFFFF
|
||||
dat[4] |= p.ANGLE_MASK | ((counter % p.COUNTER_MODULUS) << p.COUNTER_SHIFT)
|
||||
dat[5], dat[6] = raw >> 8, raw & 255
|
||||
dat[7] = p.VERSION | (checksum4(dat) << p.CHECKSUM_SHIFT)
|
||||
return addr, bytes(dat), bus
|
||||
|
||||
|
||||
def lateral(packer, bus, command, *, canfd, counter):
|
||||
values = {"HandsOffCnfm_B_Rq": 0, "LatCtlRampType_D_Rq": 2 if command.active else 0,
|
||||
"LatCtlPrecision_D_Rq": command.precision, "LatCtlPathOffst_L_Actl": 0.0,
|
||||
"LatCtlPath_An_Actl": -command.path_angle, "LatCtlCurv_No_Actl": 0.0}
|
||||
if canfd:
|
||||
values.update({"LatCtl_D2_Rq": int(command.active), "LatCtlCrv_NoRate2_Actl": 0.0,
|
||||
"LatCtlPath_No_Cnt": counter % 16, "LatCtlPath_No_Cs": 0})
|
||||
dat = packer.make_can_msg("LateralMotionControl2", bus, values)[1]
|
||||
fields = ((dat[2] << 3) | (dat[3] >> 5), (dat[6] << 3) | (dat[7] >> 5),
|
||||
((dat[3] & 31) << 6) | (dat[4] >> 2), ((dat[4] & 3) << 8) | dat[5])
|
||||
total = int(command.active) + counter % 16 + sum(v + (v >> 8) for v in fields)
|
||||
values["LatCtlPath_No_Cs"] = 255 - (total & 255)
|
||||
return packer.make_can_msg("LateralMotionControl2", bus, values)
|
||||
values.update({"LatCtl_D_Rq": int(command.active), "LatCtlRng_L_Max": 0, "LatCtlCurv_NoRate_Actl": 0.0})
|
||||
return packer.make_can_msg("LateralMotionControl", bus, values)
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Angle-primary Ford steering adapted from Alan Polk's BluePilot bp-7.0.
|
||||
|
||||
Source e1d051d7ba270261b4455068bd68f1a58db15a4a lateral_angle_ext.py.
|
||||
See ../LICENSE, ../LICENSE.md and ../CREDITS.md. This core has no vehicle,
|
||||
openpilot, Params, messaging or model-constant imports.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
from ..params import limits as bounds
|
||||
from .trim import LaneCenterTrim, LaneGeometry, TrimConfig
|
||||
|
||||
|
||||
def clip(value, low, high):
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def interp(value, xs, ys):
|
||||
if value <= xs[0]:
|
||||
return ys[0]
|
||||
for i in range(1, len(xs)):
|
||||
if value < xs[i]:
|
||||
return ys[i - 1] + (ys[i] - ys[i - 1]) * (value - xs[i - 1]) / (xs[i] - xs[i - 1])
|
||||
return ys[-1]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Inputs:
|
||||
active: bool
|
||||
speed: float
|
||||
measured_curvature: float
|
||||
desired_curvature: float
|
||||
steering_pressed: bool = False
|
||||
steering_angle: float = 0.0
|
||||
yaw_predictions: tuple = ()
|
||||
time_indices: tuple = ()
|
||||
lateral_delay: float = 0.15
|
||||
lane_change: bool = False
|
||||
lane_direction: int = 0
|
||||
limit_status: int = 0
|
||||
geometry: LaneGeometry | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Command:
|
||||
active: bool = False
|
||||
path_angle: float = 0.0
|
||||
shadow_curvature: float = 0.0
|
||||
precision: int = 1
|
||||
|
||||
|
||||
class AngleController:
|
||||
def __init__(self, *, low_gain=1.0, high_gain=1.15, trim_config=None):
|
||||
self.trim_config = TrimConfig() if trim_config is None else trim_config
|
||||
self.trim = LaneCenterTrim()
|
||||
self.low_gain = low_gain
|
||||
self.high_gain = high_gain
|
||||
self.path_last = 0.0
|
||||
self.speed_last = 0.1
|
||||
self.desired_last = 0.0
|
||||
self.human_timer = 0.0
|
||||
self.press_last = False
|
||||
self.preturned = False
|
||||
self.press_timer = 0.0
|
||||
self.blip_frames = 0
|
||||
self.blip_cooldown = 0.0
|
||||
self.blip_hold = 0.0
|
||||
self.blip_count = 0
|
||||
|
||||
def reset(self):
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
self.speed_last = 0.1
|
||||
self.desired_last = 0.0
|
||||
self.human_timer = 0.0
|
||||
self.press_last = False
|
||||
self.preturned = False
|
||||
self.press_timer = 0.0
|
||||
self.blip_frames = 0
|
||||
self.blip_cooldown = 0.0
|
||||
self.blip_hold = 0.0
|
||||
self.blip_count = 0
|
||||
|
||||
def step(self, i: Inputs) -> Command:
|
||||
values = (i.speed, i.measured_curvature, i.desired_curvature, i.steering_angle, i.lateral_delay)
|
||||
if not i.active or not all(math.isfinite(v) for v in values) or i.speed < 0.0:
|
||||
self.reset()
|
||||
return Command()
|
||||
if i.steering_pressed and not self.press_last:
|
||||
self.preturned = abs(i.steering_angle) > 45.0
|
||||
self.press_last = i.steering_pressed
|
||||
self.human_timer = self.human_timer + 0.05 if i.steering_pressed and abs(i.steering_angle) > 45.0 else 0.0
|
||||
if self.human_timer + 1e-9 >= (3.0 if self.preturned else 1.5):
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
self.desired_last = i.desired_curvature
|
||||
self.blip_frames = self.blip_count = 0
|
||||
self.blip_hold = self.blip_cooldown = self.press_timer = 0.0
|
||||
return Command()
|
||||
if i.steering_pressed:
|
||||
self.press_timer += 0.05
|
||||
else:
|
||||
if self.press_timer >= 0.5 and self.blip_cooldown <= 0.0 and self.blip_frames <= 0 and abs(self.path_last) < 0.1:
|
||||
self.blip_frames = 6
|
||||
self.press_timer = 0.0
|
||||
if self.blip_frames > 0:
|
||||
self.blip_frames -= 1
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
self.desired_last = i.desired_curvature
|
||||
if self.blip_frames == 0:
|
||||
self.blip_cooldown = 2.0
|
||||
return Command()
|
||||
|
||||
base = clip(i.lateral_delay, 0.1, 0.15) + 0.05
|
||||
predictions_valid = (len(i.yaw_predictions) == len(i.time_indices) and len(i.time_indices) >= 17 and
|
||||
all(math.isfinite(v) for v in (*i.yaw_predictions, *i.time_indices)) and
|
||||
all(b > a for a, b in zip(i.time_indices, i.time_indices[1:], strict=False)))
|
||||
curves = tuple(y / max(0.01, i.speed) for y in i.yaw_predictions) if predictions_valid else ()
|
||||
entering = bool(curves) and abs(interp(base, i.time_indices, curves)) > abs(i.desired_curvature)
|
||||
factor = 1.0 if entering else interp(abs(i.desired_curvature), (0.005, 0.020), (1.0, 0.0))
|
||||
lookup = base + bounds.VLT_EXTRA * interp(i.speed, (11.176, 24.5872), (1.0, 0.0)) * factor
|
||||
predicted = interp(lookup, i.time_indices, curves) if curves else i.desired_curvature
|
||||
saturated = i.limit_status >= 2 or self.path_last >= 0.5235 * 0.9 or self.path_last <= -0.5 * 0.9
|
||||
falling = abs(i.desired_curvature) < abs(self.desired_last) - 0.010
|
||||
blend = bounds.BLEND * 0.25 if not entering and (i.limit_status >= 1 or saturated or falling) else bounds.BLEND
|
||||
requested = predicted * blend + i.desired_curvature * (1.0 - blend)
|
||||
self.desired_last = i.desired_curvature
|
||||
precision = 0 if i.lane_change and ((i.lane_direction == 1 and requested < 0) or
|
||||
(i.lane_direction == 2 and requested > 0)) else 1
|
||||
# BluePilot's default lane-change factors are both 1.0.
|
||||
# Trim requested curvature before every existing actuator/safety limiter.
|
||||
requested = self.trim.update(requested, i.speed, i.geometry, self.trim_config,
|
||||
active=i.active, lane_change=i.lane_change, steering_pressed=i.steering_pressed)
|
||||
kappa = clip(requested, i.measured_curvature - 0.002, i.measured_curvature + 0.002) if i.speed > 9.0 else requested
|
||||
low = interp(i.speed, (13.5, 26.82), (1.0, self.low_gain))
|
||||
high = interp(i.speed, (13.5, 26.82), (1.3, self.high_gain))
|
||||
gain = interp(abs(kappa), (0.0007, 0.001), (low, high))
|
||||
path = kappa * i.speed * gain
|
||||
if saturated:
|
||||
last_mag, magnitude = abs(self.path_last), abs(path)
|
||||
if magnitude > last_mag:
|
||||
path = self.path_last
|
||||
elif last_mag - magnitude > 0.02:
|
||||
path = math.copysign(last_mag - 0.02, self.path_last)
|
||||
elif i.limit_status >= 1:
|
||||
path = clip(path, -abs(self.path_last), abs(self.path_last))
|
||||
roc = interp(i.speed, (10.0, 15.0, 25.0), (0.055, 0.0425, 0.009))
|
||||
path = clip(clip(path, -0.5235, 0.5), self.path_last - roc, self.path_last + roc)
|
||||
|
||||
# Tightening relative to bp-7.0: the shadow describes the final actuator,
|
||||
# including ROC/saturation. A truthful derived curvature must fit both the
|
||||
# measured-curvature interval and the modern ISO envelope. If those bands
|
||||
# cannot meet the previous actuator ROC, send a neutral mode-0 frame.
|
||||
effective_speed = max(i.speed, 0.1)
|
||||
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(i.speed, 1.0) ** 2)
|
||||
low_kappa, high_kappa = -cap, cap
|
||||
if i.speed > 9.0:
|
||||
low_kappa = max(low_kappa, i.measured_curvature - 0.0018)
|
||||
high_kappa = min(high_kappa, i.measured_curvature + 0.0018)
|
||||
# Native cannot trust a host-selected feel gain. Intersect every admitted
|
||||
# gain (0.95..1.3), so the worst equivalent curvature remains bounded.
|
||||
lower_gain = bounds.GAIN_MAX if low_kappa >= 0.0 else bounds.GAIN_MIN
|
||||
upper_gain = bounds.GAIN_MIN if high_kappa >= 0.0 else bounds.GAIN_MAX
|
||||
lower = max(-bounds.PATH_MAX, low_kappa * effective_speed * lower_gain, self.path_last - roc)
|
||||
upper = min(-bounds.PATH_MIN, high_kappa * effective_speed * upper_gain, self.path_last + roc)
|
||||
# Bound the actual-angle equivalent derivative at the worst admitted gain.
|
||||
# 45ms core margin remains inside the native elapsed-time/50ms cap.
|
||||
jerk_delta = bounds.LATERAL_JERK / max(i.speed - 1.0, 1.0) ** 2 * bounds.CORE_STEER_PERIOD_S * bounds.GAIN_MIN * effective_speed
|
||||
jerk_center = self.path_last / self.speed_last * effective_speed
|
||||
lower = max(lower, jerk_center - jerk_delta)
|
||||
upper = min(upper, jerk_center + jerk_delta)
|
||||
lower += bounds.PATH_ROUNDING_MARGIN
|
||||
upper -= bounds.PATH_ROUNDING_MARGIN
|
||||
if lower > upper:
|
||||
self.trim.reset()
|
||||
self.path_last = 0.0
|
||||
return Command()
|
||||
path = clip(path, lower, upper)
|
||||
self.path_last = path
|
||||
self.speed_last = effective_speed
|
||||
shadow = path / (effective_speed * gain)
|
||||
|
||||
self.blip_cooldown = max(0.0, self.blip_cooldown - 0.05)
|
||||
gap = i.desired_curvature - i.measured_curvature
|
||||
stalled = (not i.steering_pressed and not i.lane_change and i.speed > 9.0 and abs(gap) > 0.004 and
|
||||
abs(i.desired_curvature) > abs(i.measured_curvature))
|
||||
if stalled:
|
||||
if abs(kappa - requested) > 1e-9 and self.blip_cooldown <= 0.0:
|
||||
self.blip_hold += 0.05
|
||||
if self.blip_hold >= 0.5 and self.blip_count < 3 and abs(path) < 0.1:
|
||||
self.blip_frames = 6
|
||||
self.blip_hold = 0.0
|
||||
self.blip_count += 1
|
||||
else:
|
||||
self.blip_hold = 0.0
|
||||
if i.steering_pressed or abs(gap) < 0.002:
|
||||
self.blip_count = 0
|
||||
return Command(True, path, shadow, precision)
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Pure angle lane-centering trim adapted from BluePilot bp-dev e22afa6b.
|
||||
|
||||
Alan Polk and ghbarker; the source also credits StarPilot/u/jc01rho.
|
||||
See ../CREDITS.md, ../LICENSE and ../LICENSE.md. Geometry/confidence/fallback,
|
||||
filter and slew follow lane_center_trim.py; this module receives immutable arrays
|
||||
instead of a host model. Stale geometry and driver pressure additionally suppress
|
||||
trim, and nonfinite confidence cannot create correction.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
from ..params.trim import DEFAULTS, BOUNDS
|
||||
|
||||
|
||||
def clip(value, low, high):
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def interp(value, xs, ys):
|
||||
if value <= xs[0]:
|
||||
return ys[0]
|
||||
for index in range(1, len(xs)):
|
||||
if value < xs[index]:
|
||||
return ys[index - 1] + (ys[index] - ys[index - 1]) * (value - xs[index - 1]) / (xs[index] - xs[index - 1])
|
||||
return ys[-1]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrimConfig:
|
||||
enabled: bool = DEFAULTS["enabled"]
|
||||
offset_m: float = DEFAULTS["offset_m"]
|
||||
gain: float = DEFAULTS["gain"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LaneGeometry:
|
||||
position_x: tuple = ()
|
||||
position_y: tuple = ()
|
||||
left_x: tuple = ()
|
||||
left_y: tuple = ()
|
||||
right_x: tuple = ()
|
||||
right_y: tuple = ()
|
||||
probabilities: tuple = ()
|
||||
standard_deviations: tuple = ()
|
||||
fresh: bool = False
|
||||
|
||||
|
||||
def sample(xs, ys, distance):
|
||||
if (not isinstance(xs, tuple) or not isinstance(ys, tuple) or len(xs) < 2 or len(xs) != len(ys) or
|
||||
not all(math.isfinite(v) for v in (*xs, *ys)) or not all(b > a for a, b in zip(xs, xs[1:], strict=False))):
|
||||
return None
|
||||
return interp(distance, xs, ys)
|
||||
|
||||
|
||||
class LaneCenterTrim:
|
||||
def __init__(self):
|
||||
self.correction = 0.0
|
||||
|
||||
def reset(self):
|
||||
self.correction = 0.0
|
||||
|
||||
def update(self, requested, speed, geometry, config, *, active, lane_change, steering_pressed=False):
|
||||
try:
|
||||
valid = (isinstance(config, TrimConfig) and config.enabled is True and active and not lane_change and
|
||||
not steering_pressed and isinstance(geometry, LaneGeometry) and geometry.fresh is True and
|
||||
math.isfinite(requested) and math.isfinite(speed) and speed >= 0.0 and
|
||||
math.isfinite(config.offset_m) and BOUNDS["offset_m"][0] <= config.offset_m <= BOUNDS["offset_m"][1] and
|
||||
math.isfinite(config.gain) and BOUNDS["gain"][0] <= config.gain <= BOUNDS["gain"][1])
|
||||
authority = interp(speed, BOUNDS["speed_mps"], BOUNDS["speed_authority"]) if valid else 0.0
|
||||
if not valid or authority <= 0.0:
|
||||
self.reset()
|
||||
return requested
|
||||
distance = clip(speed, *BOUNDS["lookahead_m"])
|
||||
model_y = sample(geometry.position_x, geometry.position_y, distance)
|
||||
if model_y is None:
|
||||
self.reset()
|
||||
return requested
|
||||
# Bad lane-only geometry retains the source's model-position/offset fallback.
|
||||
scale, center = self.lane_blend(geometry, distance)
|
||||
error = scale * (center - model_y) + config.offset_m
|
||||
raw = 2.0 * error / distance ** 2
|
||||
target = clip(raw, -BOUNDS["raw_correction"], BOUNDS["raw_correction"]) * config.gain * authority
|
||||
alpha = 1.0 - math.exp(-BOUNDS["period_s"] / BOUNDS["tau_s"])
|
||||
filtered = alpha * target + (1.0 - alpha) * self.correction
|
||||
self.correction = clip(filtered, self.correction - BOUNDS["correction_delta"], self.correction + BOUNDS["correction_delta"])
|
||||
return requested + self.correction
|
||||
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
|
||||
self.reset()
|
||||
return requested
|
||||
|
||||
def lane_blend(self, geometry, distance):
|
||||
try:
|
||||
left = sample(geometry.left_x, geometry.left_y, distance)
|
||||
right = sample(geometry.right_x, geometry.right_y, distance)
|
||||
probs, stds = geometry.probabilities, geometry.standard_deviations
|
||||
if (left is None or right is None or not isinstance(probs, tuple) or not isinstance(stds, tuple) or
|
||||
len(probs) != 2 or len(stds) != 2 or not all(math.isfinite(p) and 0.0 <= p <= 1.0 for p in probs) or
|
||||
not all(math.isfinite(s) and s >= 0.0 for s in stds)):
|
||||
return 0.0, 0.0
|
||||
confidence = min(*probs, interp(right - left, BOUNDS["width_m"], BOUNDS["width_tolerance"]),
|
||||
interp(max(stds), BOUNDS["std"], BOUNDS["std_tolerance"]))
|
||||
scale = clip(interp(confidence, BOUNDS["confidence"], (0.0, 1.0)), 0.0, 1.0)
|
||||
return scale, 0.5 * (left + right)
|
||||
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
|
||||
return 0.0, 0.0
|
||||
@@ -0,0 +1,101 @@
|
||||
"""StarPilot's CP adapter; model transport and saved Params stay in the host.
|
||||
|
||||
Only exact final CP words derived from the existing five classic/six CANFD
|
||||
extended profiles can select angle. AOL and Transit keep their current owner.
|
||||
"""
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs
|
||||
from opendbc.bluepilot_lateral.core.trim import LaneGeometry
|
||||
from opendbc.bluepilot_lateral.params import limits as bounds
|
||||
|
||||
ANGLE_FLAG = 128
|
||||
CLASSIC = frozenset({CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_ESCAPE_MK4, CAR.FORD_FOCUS_MK4,
|
||||
CAR.FORD_MAVERICK_MK1, CAR.FORD_EXPLORER_MK6})
|
||||
CANFD = frozenset({CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4, CAR.FORD_F_150_MK14,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2, CAR.FORD_MUSTANG_MACH_E_MK1})
|
||||
TRUCKS = CANFD - {CAR.FORD_ESCAPE_MK4_5, CAR.FORD_MUSTANG_MACH_E_MK1}
|
||||
|
||||
|
||||
def mode_word(cp, *, selected):
|
||||
if (cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience != 0 or
|
||||
cp.steerControlType != structs.CarParams.SteerControlType.angle or
|
||||
not cp.pcmCruise or len(cp.safetyConfigs) not in (1, 2)):
|
||||
return None
|
||||
if len(cp.safetyConfigs) == 2 and (cp.safetyConfigs[0].safetyModel != structs.CarParams.SafetyModel.noOutput or
|
||||
cp.safetyConfigs[0].safetyParam != 0):
|
||||
return None
|
||||
fd = cp.carFingerprint in CANFD
|
||||
if ((fd and (not cp.flags & FordFlags.CANFD or cp.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM))) or
|
||||
(not fd and (cp.carFingerprint not in CLASSIC or cp.flags & ~int(FordFlags.HAS_BSM)))):
|
||||
return None
|
||||
safety = cp.safetyConfigs[-1]
|
||||
long = int(cp.openpilotLongitudinalControl)
|
||||
original = (18 if cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 else 66) + long if fd else 32 + long
|
||||
expected = ANGLE_FLAG + (2 if fd else 0) + long if selected else original
|
||||
return expected if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected else None
|
||||
|
||||
|
||||
def supported(cp):
|
||||
return mode_word(cp, selected=False) is not None or mode_word(cp, selected=True) is not None
|
||||
|
||||
|
||||
def qualified(cp):
|
||||
return mode_word(cp, selected=True) is not None
|
||||
|
||||
|
||||
def select(cp, raw):
|
||||
# Absence/invalid bytes stay at current curvature. Never rewrite saved bytes.
|
||||
chosen = (type(raw) is int and raw == 1) or (type(raw) in (bytes, str) and raw in (b"1", "1"))
|
||||
if not chosen or mode_word(cp, selected=False) is None:
|
||||
return False
|
||||
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
|
||||
cp.steerActuatorDelay = 0.22
|
||||
return True
|
||||
|
||||
|
||||
def model_geometry(model, *, fresh=False):
|
||||
"""Project a caller-qualified snapshot to immutable core arrays; no model imports."""
|
||||
if model is None or fresh is not True:
|
||||
return None
|
||||
try:
|
||||
position_x, position_y = tuple(model.position.x), tuple(model.position.y)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
return None
|
||||
try:
|
||||
return LaneGeometry(position_x=position_x, position_y=position_y, fresh=True, left_x=tuple(model.laneLines[1].x), left_y=tuple(model.laneLines[1].y),
|
||||
right_x=tuple(model.laneLines[2].x), right_y=tuple(model.laneLines[2].y),
|
||||
probabilities=(model.laneLineProbs[1], model.laneLineProbs[2]),
|
||||
standard_deviations=(model.laneLineStds[1], model.laneLineStds[2]))
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return LaneGeometry(position_x=position_x, position_y=position_y, fresh=True)
|
||||
|
||||
|
||||
class Host:
|
||||
def __init__(self, cp):
|
||||
if not qualified(cp):
|
||||
raise ValueError("BluePilot angle requires an exact admitted Ford safety word")
|
||||
low, high = (0.95, 0.95) if cp.carFingerprint in TRUCKS else ((1.0, 1.05) if cp.carFingerprint in CANFD else (1.0, 1.15))
|
||||
self.cp = cp
|
||||
self.controller = AngleController(low_gain=low, high_gain=high)
|
||||
self.inputs = None
|
||||
|
||||
def update(self, CC, CS):
|
||||
if self.inputs is not None:
|
||||
self.inputs.update()
|
||||
snapshot = self.inputs.lateral_snapshot(CS.out.vEgoRaw) if self.inputs is not None else None
|
||||
model, times, delay, _ = snapshot if snapshot is not None else (None, (), 0.15, True)
|
||||
ready = (self.inputs is not None and self.inputs.native_permission(self.cp, max_age_ns=bounds.PANDA_AGE_NS) and
|
||||
CC.enabled and CC.latActive and CS.out.cruiseState.enabled and CS.out.canValid and
|
||||
not CS.out.canTimeout and not CS.out.gasPressed and not CS.out.brakePressed and
|
||||
not CS.out.steerFaultTemporary and not CS.out.steerFaultPermanent and not CS.out.vehicleSensorsInvalid)
|
||||
meta = model.meta if model is not None else None
|
||||
return self.controller.step(Inputs(
|
||||
active=ready, speed=CS.out.vEgoRaw, measured_curvature=-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1),
|
||||
desired_curvature=CC.actuators.curvature, steering_pressed=CS.out.steeringPressed,
|
||||
steering_angle=CS.out.steeringAngleDeg, yaw_predictions=tuple(model.orientationRate.z) if model is not None else (),
|
||||
time_indices=tuple(times), lateral_delay=delay,
|
||||
lane_change=int(getattr(meta.laneChangeState, "raw", meta.laneChangeState)) in (1, 2, 3) if meta else False,
|
||||
lane_direction=int(getattr(meta.laneChangeDirection, "raw", meta.laneChangeDirection)) if meta else 0,
|
||||
limit_status=getattr(CS, "lat_ctl_lim_stat", 0),
|
||||
geometry=model_geometry(model, fresh=snapshot is not None)))
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Generate protocol constants without loading a host or vehicle module."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(__file__).parent
|
||||
spec = json.loads((root / "protocol.json").read_text())
|
||||
(root / "protocol.py").write_text('"""Generated from protocol.json; layout preserves the existing curvature flag."""\n' +
|
||||
"".join(f"{key.upper()} = {value!r}\n" for key, value in spec.items()))
|
||||
(root / "protocol.h").write_text("#pragma once\n// Generated from protocol.json; do not allocate bit 1 to angle metadata.\n" +
|
||||
"".join(f"#define FORD_BP_{key.upper()} {int(value)}U\n" for key, value in spec.items() if key != "shadow_scale"))
|
||||
|
||||
manifest = json.loads((root / "manifest.json").read_text())
|
||||
defaults, bounds = manifest["defaults"], manifest["bounds"]
|
||||
limits = {"BLEND": defaults["blend"], "VLT_EXTRA": defaults["vlt_extra_max"],
|
||||
"PATH_MIN": bounds["path_angle_wire"][0], "PATH_MAX": bounds["path_angle_wire"][1],
|
||||
"GAIN_MIN": bounds["gain"][0], "GAIN_MAX": bounds["gain"][1],
|
||||
"CURVATURE_MAX": bounds["curvature"], "CURVATURE_ERROR": bounds["curvature_error"],
|
||||
"LATERAL_ACCEL": bounds["lateral_accel"], "RX_AGE_US": bounds["rx_age_us"],
|
||||
"SHADOW_AGE_US": bounds["shadow_age_us"], "PATH_ROUNDING_MARGIN": bounds["path_rounding_margin"],
|
||||
"SHADOW_ROUNDING_MARGIN": bounds["shadow_rounding_margin"], "LATERAL_JERK": bounds["lateral_jerk"],
|
||||
"RT_INTERVAL_US": bounds["rt_interval_us"], "STEER_PERIOD_US": bounds["steer_period_us"],
|
||||
"CORE_STEER_PERIOD_S": bounds["core_steer_period_s"], "PANDA_AGE_NS": bounds["panda_age_ns"]}
|
||||
(root / "limits.py").write_text('"""Generated from manifest.json; host and core use this manifest contract."""\n' +
|
||||
"".join(f"{key} = {value!r}\n" for key, value in limits.items()))
|
||||
(root / "limits.h").write_text("#pragma once\n// Generated from manifest.json; native and Python bounds share a source.\n" +
|
||||
"".join(f"#define FORD_BP_{key} {str(value) + 'F' if isinstance(value, float) else str(value) + 'U'}\n"
|
||||
for key, value in limits.items()))
|
||||
|
||||
# Optional angle trim settings are Python-only; native safety limits stay unchanged.
|
||||
trim = manifest["angle_lane_trim"]
|
||||
types = {"enabled": "bool", "offset_m": "float", "gain": "float"}
|
||||
defaults_fields = "".join(f" {key}: {types[key]}\n" for key in trim["defaults"])
|
||||
bounds_fields = "".join(f" {key}: {'list[float]' if isinstance(value, list) else 'float'}\n" for key, value in trim["bounds"].items())
|
||||
(root / "trim.py").write_text('"""Generated Python-only angle trim defaults; no native bound or registry changes."""\n' +
|
||||
"from typing import TypedDict\n\n\n" +
|
||||
"class TrimDefaults(TypedDict):\n" + defaults_fields + "\n\n" +
|
||||
"class TrimBounds(TypedDict):\n" + bounds_fields + "\n\n" +
|
||||
f"SOURCE_REVISION = {trim['source_revision']!r}\n" +
|
||||
f"DEFAULTS: TrimDefaults = {trim['defaults']!r}\n" +
|
||||
"BOUNDS: TrimBounds = {\n" +
|
||||
"".join(f" {key!r}: {value!r},\n" for key, value in trim["bounds"].items()) + "}\n")
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
// Generated from manifest.json; native and Python bounds share a source.
|
||||
#define FORD_BP_BLEND 0.5F
|
||||
#define FORD_BP_VLT_EXTRA 0.1F
|
||||
#define FORD_BP_PATH_MIN -0.5F
|
||||
#define FORD_BP_PATH_MAX 0.5235F
|
||||
#define FORD_BP_GAIN_MIN 0.95F
|
||||
#define FORD_BP_GAIN_MAX 1.3F
|
||||
#define FORD_BP_CURVATURE_MAX 0.02F
|
||||
#define FORD_BP_CURVATURE_ERROR 0.002F
|
||||
#define FORD_BP_LATERAL_ACCEL 2.4114F
|
||||
#define FORD_BP_RX_AGE_US 100000U
|
||||
#define FORD_BP_SHADOW_AGE_US 60000U
|
||||
#define FORD_BP_PATH_ROUNDING_MARGIN 0.0005F
|
||||
#define FORD_BP_SHADOW_ROUNDING_MARGIN 1e-06F
|
||||
#define FORD_BP_LATERAL_JERK 3.5886F
|
||||
#define FORD_BP_RT_INTERVAL_US 250000U
|
||||
#define FORD_BP_STEER_PERIOD_US 50000U
|
||||
#define FORD_BP_CORE_STEER_PERIOD_S 0.045F
|
||||
#define FORD_BP_PANDA_AGE_NS 300000000U
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Generated from manifest.json; host and core use this manifest contract."""
|
||||
BLEND = 0.5
|
||||
VLT_EXTRA = 0.1
|
||||
PATH_MIN = -0.5
|
||||
PATH_MAX = 0.5235
|
||||
GAIN_MIN = 0.95
|
||||
GAIN_MAX = 1.3
|
||||
CURVATURE_MAX = 0.02
|
||||
CURVATURE_ERROR = 0.002
|
||||
LATERAL_ACCEL = 2.4114
|
||||
RX_AGE_US = 100000
|
||||
SHADOW_AGE_US = 60000
|
||||
PATH_ROUNDING_MARGIN = 0.0005
|
||||
SHADOW_ROUNDING_MARGIN = 1e-06
|
||||
LATERAL_JERK = 3.5886
|
||||
RT_INTERVAL_US = 250000
|
||||
STEER_PERIOD_US = 50000
|
||||
CORE_STEER_PERIOD_S = 0.045
|
||||
PANDA_AGE_NS = 300000000
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"source_revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a",
|
||||
"settings": [
|
||||
{
|
||||
"key": "FordLateralMode",
|
||||
"type": "enum",
|
||||
"default": 0,
|
||||
"values": {
|
||||
"0": "Current curvature",
|
||||
"1": "BluePilot angle"
|
||||
},
|
||||
"label": "Ford steering controller",
|
||||
"description": "Select the steering controller for supported Ford profiles. Takes effect on the next drive.",
|
||||
"modes": [
|
||||
"angle"
|
||||
]
|
||||
}
|
||||
],
|
||||
"defaults": {
|
||||
"blend": 0.5,
|
||||
"vlt_extra_max": 0.1,
|
||||
"low_speed_factor": 1.0,
|
||||
"high_speed_factor": 1.0,
|
||||
"dampening": 1.0,
|
||||
"lane_change_factor": 1.0
|
||||
},
|
||||
"bounds": {
|
||||
"path_angle_wire": [
|
||||
-0.5,
|
||||
0.5235
|
||||
],
|
||||
"gain": [
|
||||
0.95,
|
||||
1.3
|
||||
],
|
||||
"curvature_error": 0.002,
|
||||
"lateral_accel": 2.4114,
|
||||
"rx_age_us": 100000,
|
||||
"shadow_age_us": 60000,
|
||||
"steer_period_us": 50000,
|
||||
"curvature": 0.02,
|
||||
"path_rounding_margin": 0.0005,
|
||||
"shadow_rounding_margin": 1e-06,
|
||||
"lateral_jerk": 3.5886,
|
||||
"rt_interval_us": 250000,
|
||||
"core_steer_period_s": 0.045,
|
||||
"panda_age_ns": 300000000
|
||||
},
|
||||
"angle_lane_trim": {
|
||||
"source_revision": "e22afa6be9b881fa784c92ebb316db47728a3d81",
|
||||
"defaults": {
|
||||
"enabled": false,
|
||||
"offset_m": 0.0,
|
||||
"gain": 0.25
|
||||
},
|
||||
"bounds": {
|
||||
"offset_m": [
|
||||
-0.5,
|
||||
0.5
|
||||
],
|
||||
"gain": [
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"width_m": [
|
||||
2.4,
|
||||
2.8,
|
||||
3.75,
|
||||
4.25
|
||||
],
|
||||
"width_tolerance": [
|
||||
0.0,
|
||||
0.81,
|
||||
0.81,
|
||||
0.59
|
||||
],
|
||||
"std": [
|
||||
0.3,
|
||||
0.5
|
||||
],
|
||||
"std_tolerance": [
|
||||
0.81,
|
||||
0.0
|
||||
],
|
||||
"confidence": [
|
||||
0.6,
|
||||
0.8
|
||||
],
|
||||
"speed_mps": [
|
||||
0.0,
|
||||
9.0,
|
||||
15.0
|
||||
],
|
||||
"speed_authority": [
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"lookahead_m": [
|
||||
8.0,
|
||||
35.0
|
||||
],
|
||||
"raw_correction": 0.004,
|
||||
"tau_s": 0.4,
|
||||
"period_s": 0.05,
|
||||
"correction_delta": 0.00015
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
// Generated from protocol.json; do not allocate bit 1 to angle metadata.
|
||||
#define FORD_BP_VERSION 1U
|
||||
#define FORD_BP_ANGLE_MASK 1U
|
||||
#define FORD_BP_EXTENDED_CURVATURE_MASK 2U
|
||||
#define FORD_BP_COUNTER_SHIFT 2U
|
||||
#define FORD_BP_COUNTER_MASK 28U
|
||||
#define FORD_BP_COUNTER_MODULUS 8U
|
||||
#define FORD_BP_VERSION_MASK 15U
|
||||
#define FORD_BP_CHECKSUM_SHIFT 4U
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"angle_mask": 1,
|
||||
"extended_curvature_mask": 2,
|
||||
"counter_shift": 2,
|
||||
"counter_mask": 28,
|
||||
"counter_modulus": 8,
|
||||
"shadow_scale": 1e-06,
|
||||
"version_mask": 15,
|
||||
"checksum_shift": 4
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Generated from protocol.json; layout preserves the existing curvature flag."""
|
||||
VERSION = 1
|
||||
ANGLE_MASK = 1
|
||||
EXTENDED_CURVATURE_MASK = 2
|
||||
COUNTER_SHIFT = 2
|
||||
COUNTER_MASK = 28
|
||||
COUNTER_MODULUS = 8
|
||||
SHADOW_SCALE = 1e-06
|
||||
VERSION_MASK = 15
|
||||
CHECKSUM_SHIFT = 4
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Generated Python-only angle trim defaults; no native bound or registry changes."""
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class TrimDefaults(TypedDict):
|
||||
enabled: bool
|
||||
offset_m: float
|
||||
gain: float
|
||||
|
||||
|
||||
class TrimBounds(TypedDict):
|
||||
offset_m: list[float]
|
||||
gain: list[float]
|
||||
width_m: list[float]
|
||||
width_tolerance: list[float]
|
||||
std: list[float]
|
||||
std_tolerance: list[float]
|
||||
confidence: list[float]
|
||||
speed_mps: list[float]
|
||||
speed_authority: list[float]
|
||||
lookahead_m: list[float]
|
||||
raw_correction: float
|
||||
tau_s: float
|
||||
period_s: float
|
||||
correction_delta: float
|
||||
|
||||
|
||||
SOURCE_REVISION = 'e22afa6be9b881fa784c92ebb316db47728a3d81'
|
||||
DEFAULTS: TrimDefaults = {'enabled': False, 'offset_m': 0.0, 'gain': 0.25}
|
||||
BOUNDS: TrimBounds = {
|
||||
'offset_m': [-0.5, 0.5],
|
||||
'gain': [0.0, 1.0],
|
||||
'width_m': [2.4, 2.8, 3.75, 4.25],
|
||||
'width_tolerance': [0.0, 0.81, 0.81, 0.59],
|
||||
'std': [0.3, 0.5],
|
||||
'std_tolerance': [0.81, 0.0],
|
||||
'confidence': [0.6, 0.8],
|
||||
'speed_mps': [0.0, 9.0, 15.0],
|
||||
'speed_authority': [0.0, 0.0, 1.0],
|
||||
'lookahead_m': [8.0, 35.0],
|
||||
'raw_correction': 0.004,
|
||||
'tau_s': 0.4,
|
||||
'period_s': 0.05,
|
||||
'correction_delta': 0.00015,
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
#pragma once
|
||||
// Angle-primary Ford extension adapted from Alan Polk's bp-7.0 e1d051d7.
|
||||
// See ../CREDITS.md, ../LICENSE and ../LICENSE.md. No reset bypass.
|
||||
#include "opendbc/bluepilot_lateral/params/protocol.h"
|
||||
#include "opendbc/bluepilot_lateral/params/limits.h"
|
||||
|
||||
// Shared Ford stock-switch state is declared before the angle hooks below.
|
||||
static bool ford_stock_switch = false;
|
||||
static bool ford_bp_enabled = false;
|
||||
static bool ford_bp_canfd = false;
|
||||
static bool ford_bp_shadow_seen = false;
|
||||
static bool ford_bp_lka_counter_seen = false;
|
||||
static uint8_t ford_bp_lka_counter = 0U;
|
||||
static int ford_bp_shadow_raw = 0;
|
||||
static uint32_t ford_bp_shadow_us = 0U;
|
||||
static int ford_bp_path_last = 0;
|
||||
static bool ford_bp_control_counter_seen = false;
|
||||
static uint8_t ford_bp_control_counter = 0U;
|
||||
static bool ford_bp_control_seen = false;
|
||||
static uint32_t ford_bp_control_us = 0U;
|
||||
static float ford_bp_equivalent_last = 0.0F;
|
||||
static unsigned int ford_bp_rt_msgs = 0U;
|
||||
static unsigned int ford_bp_rt_msgs_prev = 0U;
|
||||
static uint32_t ford_bp_rt_us = 0U;
|
||||
static bool ford_bp_rx_seen[7];
|
||||
static uint32_t ford_bp_rx_us[7];
|
||||
|
||||
static void ford_bp_invalidate_shadow(void) {
|
||||
ford_bp_shadow_seen = false;
|
||||
ford_bp_shadow_raw = 0;
|
||||
}
|
||||
|
||||
static void ford_bp_withdraw(void) {
|
||||
ford_bp_invalidate_shadow();
|
||||
ford_bp_path_last = 0;
|
||||
ford_bp_equivalent_last = 0.0F;
|
||||
}
|
||||
|
||||
static bool ford_bp_configure(uint16_t param) {
|
||||
ford_bp_enabled = ((param == 128U) || (param == 129U) || (param == 130U)) && (alternative_experience == 0);
|
||||
#ifdef ALLOW_DEBUG
|
||||
ford_bp_enabled |= (param == 131U) && (alternative_experience == 0);
|
||||
#endif
|
||||
ford_bp_canfd = (param & 2U) != 0U;
|
||||
ford_bp_lka_counter_seen = false;
|
||||
ford_bp_control_counter_seen = false;
|
||||
ford_bp_control_seen = false;
|
||||
ford_bp_lka_counter = 0U;
|
||||
ford_bp_control_counter = 0U;
|
||||
ford_bp_shadow_us = 0U;
|
||||
ford_bp_control_us = 0U;
|
||||
ford_bp_rt_msgs = 0U;
|
||||
ford_bp_rt_msgs_prev = 0U;
|
||||
ford_bp_rt_us = microsecond_timer_get();
|
||||
ford_bp_withdraw();
|
||||
for (unsigned int i = 0U; i < 7U; i++) {
|
||||
ford_bp_rx_seen[i] = false;
|
||||
ford_bp_rx_us[i] = 0U;
|
||||
}
|
||||
return ((param & 128U) == 0U) || ford_bp_enabled;
|
||||
}
|
||||
|
||||
static bool ford_bp_health(uint32_t now);
|
||||
|
||||
static void ford_bp_rx(const CANPacket_t *msg) {
|
||||
if (ford_bp_enabled && (msg->bus == 0U) && (GET_LEN(msg) == 8U)) {
|
||||
const unsigned int ids[7] = {0x415U, 0x202U, 0x91U, 0x165U, 0x204U, 0x213U, 0x83U};
|
||||
for (unsigned int i = 0U; i < 7U; i++) {
|
||||
if (msg->addr == ids[i]) {
|
||||
ford_bp_rx_seen[i] = true;
|
||||
ford_bp_rx_us[i] = microsecond_timer_get();
|
||||
}
|
||||
}
|
||||
// Angle-specific source freshness is stricter than the generic1s RX tick.
|
||||
// Expose that withdrawal through the ordinary public controls acknowledgment.
|
||||
if (controls_allowed && !ford_bp_health(microsecond_timer_get())) {
|
||||
controls_allowed = false;
|
||||
}
|
||||
// Permission losses remove the old actuator history before any later arm.
|
||||
if (!controls_allowed || brake_pressed || gas_pressed) {
|
||||
ford_bp_withdraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool ford_bp_health(uint32_t now) {
|
||||
bool valid = !safety_rx_checks_invalid && !relay_malfunction;
|
||||
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
|
||||
const RxStatus *status = ¤t_safety_config.rx_checks[i].status;
|
||||
valid &= status->msg_seen && !status->lagging && status->valid_checksum &&
|
||||
status->valid_quality_flag && (status->wrong_counters < MAX_WRONG_COUNTERS);
|
||||
}
|
||||
const unsigned int count = ford_stock_switch ? 7U : 6U;
|
||||
for (unsigned int i = 0U; i < count; i++) {
|
||||
const uint32_t age = ((i == 3U) || (i == 6U)) ? (2U * FORD_BP_RX_AGE_US) : FORD_BP_RX_AGE_US;
|
||||
valid &= ford_bp_rx_seen[i] && (safety_get_ts_elapsed(now, ford_bp_rx_us[i]) <= age);
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
static bool ford_bp_lka(const CANPacket_t *msg) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
const unsigned int counter = (msg->data[4] & FORD_BP_COUNTER_MASK) >> FORD_BP_COUNTER_SHIFT;
|
||||
unsigned int sum = 0U;
|
||||
for (unsigned int i = 0U; i < 7U; i++) { sum += msg->data[i]; }
|
||||
const unsigned int checksum = (15U - (sum & 15U)) & 15U;
|
||||
// Preserve every DBC-owned bit of the packer's neutral LKA body.
|
||||
const unsigned int angle = ((msg->data[2] & 0xFU) << 8) | msg->data[3];
|
||||
const unsigned int curvature = (msg->data[1] << 4) | (msg->data[2] >> 4);
|
||||
bool valid = !relay_malfunction && (msg->bus == 0U) && (GET_LEN(msg) == 8U) && (msg->data[0] == 0U) &&
|
||||
(angle == 2048U) && (curvature == 2048U) &&
|
||||
((msg->data[4] & 0xE3U) == FORD_BP_ANGLE_MASK) &&
|
||||
((msg->data[7] & FORD_BP_VERSION_MASK) == FORD_BP_VERSION) &&
|
||||
((msg->data[7] >> FORD_BP_CHECKSUM_SHIFT) == checksum);
|
||||
const int shadow = to_signed((msg->data[5] << 8) | msg->data[6], 16);
|
||||
const bool resync = (shadow == 0) && !controls_allowed;
|
||||
if (ford_bp_lka_counter_seen && !resync) {
|
||||
valid &= counter == ((ford_bp_lka_counter + 1U) % FORD_BP_COUNTER_MODULUS);
|
||||
valid &= safety_get_ts_elapsed(now, ford_bp_shadow_us) >= 20000U;
|
||||
}
|
||||
valid &= SAFETY_ABS(shadow) <= 20000;
|
||||
if (valid) {
|
||||
ford_bp_lka_counter = (uint8_t)counter;
|
||||
ford_bp_lka_counter_seen = true;
|
||||
ford_bp_shadow_raw = shadow;
|
||||
ford_bp_shadow_seen = !resync;
|
||||
ford_bp_shadow_us = now;
|
||||
} else {
|
||||
ford_bp_invalidate_shadow();
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
static bool ford_bp_lateral(const CANPacket_t *msg) {
|
||||
const bool fd = msg->addr == 0x3D6U;
|
||||
const unsigned int mode = fd ? ((msg->data[0] >> 4) & 7U) : ((msg->data[4] >> 2) & 7U);
|
||||
const unsigned int curvature = fd ? ((msg->data[2] << 3) | (msg->data[3] >> 5)) :
|
||||
((msg->data[0] << 3) | (msg->data[1] >> 5));
|
||||
const unsigned int rate = fd ? (((unsigned int)msg->data[6] << 3) | (msg->data[7] >> 5)) :
|
||||
(((msg->data[1] & 31U) << 8) | msg->data[2]);
|
||||
const unsigned int path_raw = fd ? (((msg->data[3] & 31U) << 6) | (msg->data[4] >> 2)) :
|
||||
(((unsigned int)msg->data[3] << 3) | (msg->data[4] >> 5));
|
||||
const unsigned int offset = fd ? (((msg->data[4] & 3U) << 8) | msg->data[5]) :
|
||||
(((unsigned int)msg->data[5] << 2) | (msg->data[6] >> 6));
|
||||
const unsigned int precision = fd ? ((msg->data[0] >> 2) & 3U) : (msg->data[4] & 3U);
|
||||
const unsigned int ramp = fd ? (msg->data[0] & 3U) : ((msg->data[6] >> 4) & 3U);
|
||||
const bool active = mode == 1U;
|
||||
const int path = (int)path_raw - 1000;
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
bool valid = !relay_malfunction && (msg->bus == 0U) && (GET_LEN(msg) == 8U) && (fd == ford_bp_canfd) && (mode <= 1U) &&
|
||||
(curvature == 1000U) && (rate == (fd ? 1024U : 4096U)) && (offset == 512U) &&
|
||||
(precision <= 1U) && (ramp == (active ? 2U : 0U));
|
||||
const unsigned int counter = (msg->data[7] >> 1) & 15U;
|
||||
if (fd) {
|
||||
unsigned int checksum = mode + counter;
|
||||
const unsigned int fields[4] = {curvature, rate, path_raw, offset};
|
||||
for (unsigned int i = 0U; i < 4U; i++) { checksum += fields[i] + (fields[i] >> 8); }
|
||||
valid &= msg->data[1] == (0xFFU - (checksum & 0xFFU));
|
||||
valid &= ((msg->data[0] & 0x80U) == 0U) && ((msg->data[7] & 1U) == 0U);
|
||||
if (ford_bp_control_counter_seen && active) { valid &= counter == ((ford_bp_control_counter + 1U) % 16U); }
|
||||
} else {
|
||||
valid &= ((msg->data[6] & 0xFU) == 0U) && (msg->data[7] == 0U);
|
||||
}
|
||||
const float speed_for_equivalent = SAFETY_MAX(vehicle_speed.min / VEHICLE_SPEED_FACTOR, 0.1F);
|
||||
const float equivalent = (float)path / (2000.0F * speed_for_equivalent);
|
||||
// Time-window expiration is independent of packet acceptance: denied floods
|
||||
// cannot latch the old count forever or erase accepted actuator history.
|
||||
const uint32_t rt_elapsed = safety_get_ts_elapsed(now, ford_bp_rt_us);
|
||||
if (rt_elapsed >= FORD_BP_RT_INTERVAL_US) {
|
||||
ford_bp_rt_msgs = 0U;
|
||||
ford_bp_rt_msgs_prev = 0U;
|
||||
ford_bp_rt_us = now;
|
||||
} else if (rt_elapsed >= (FORD_BP_RT_INTERVAL_US / 2U)) {
|
||||
ford_bp_rt_msgs_prev = ford_bp_rt_msgs;
|
||||
ford_bp_rt_msgs = 0U;
|
||||
ford_bp_rt_us = now;
|
||||
} else {
|
||||
// Keep the current history while neither expiration boundary has elapsed.
|
||||
}
|
||||
const unsigned int rt_count = ford_bp_rt_msgs + ford_bp_rt_msgs_prev;
|
||||
valid &= rt_count <= 7U; // Current20Hz curvature RT allowance; accepted frames own this count.
|
||||
if (active) {
|
||||
static const struct lookup_t roc = {{10.0F, 15.0F, 25.0F}, {0.0561F, 0.04335F, 0.00918F}};
|
||||
const float speed_min = SAFETY_MAX(vehicle_speed.min / VEHICLE_SPEED_FACTOR, 0.1F);
|
||||
const float speed_max = SAFETY_MAX(vehicle_speed.max / VEHICLE_SPEED_FACTOR, 0.1F);
|
||||
const float delta_float = safety_interpolate(roc, speed_min - 1.0F) * 2000.0F;
|
||||
const int delta = (int)delta_float + 1;
|
||||
const float shadow = (float)ford_bp_shadow_raw * 0.000001F;
|
||||
const float shadow_can = shadow * 50000.0F;
|
||||
const float path_abs = SAFETY_ABS(path) / 2000.0F;
|
||||
const float path_curvature_low = path_abs / (speed_max * FORD_BP_GAIN_MAX);
|
||||
const float path_curvature_high = path_abs / (speed_min * FORD_BP_GAIN_MIN);
|
||||
const float physical_cap = SAFETY_MIN(FORD_BP_CURVATURE_MAX, FORD_BP_LATERAL_ACCEL / (SAFETY_MAX(speed_max, 1.0F) * SAFETY_MAX(speed_max, 1.0F)));
|
||||
const float relation_low = SAFETY_ABS(shadow) * speed_min * FORD_BP_GAIN_MIN * 2000.0F;
|
||||
const float relation_high = SAFETY_ABS(shadow) * speed_max * FORD_BP_GAIN_MAX * 2000.0F;
|
||||
valid &= lateral_controls_allowed() && controls_allowed && ford_bp_health(now) && ford_bp_shadow_seen &&
|
||||
(safety_get_ts_elapsed(now, ford_bp_shadow_us) <= FORD_BP_SHADOW_AGE_US) &&
|
||||
(path >= -1000) && (path <= 1047) && (SAFETY_ABS(path - ford_bp_path_last) <= delta) &&
|
||||
((path == 0) || ((ford_bp_shadow_raw != 0) && ((path > 0) == (ford_bp_shadow_raw > 0)))) &&
|
||||
(SAFETY_ABS(path) >= (relation_low - 2.0F)) && (SAFETY_ABS(path) <= (relation_high + 2.0F)) &&
|
||||
(SAFETY_ABS(shadow) <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN)) && (path_curvature_high <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN));
|
||||
if (speed_max > 10.0F) {
|
||||
const int measured_min = curvature_state.meas.min - 100;
|
||||
const int measured_max = curvature_state.meas.max + 100;
|
||||
const float measured_min_float = (float)measured_min;
|
||||
const float measured_max_float = (float)measured_max;
|
||||
valid &= (shadow_can >= measured_min_float) && (shadow_can <= measured_max_float);
|
||||
// Independent actual-actuator check; an invented shadow cannot launder path angle.
|
||||
const float signed_low = (path < 0) ? (-path_curvature_high) : path_curvature_low;
|
||||
const float signed_high = (path < 0) ? (-path_curvature_low) : path_curvature_high;
|
||||
valid &= ((signed_low * 50000.0F) >= measured_min_float) &&
|
||||
((signed_high * 50000.0F) <= measured_max_float);
|
||||
}
|
||||
const uint32_t elapsed = ford_bp_control_seen ? safety_get_ts_elapsed(now, ford_bp_control_us) : FORD_BP_STEER_PERIOD_US;
|
||||
const float jerk_speed = SAFETY_MAX(speed_min - 1.0F, 1.0F);
|
||||
const float jerk_delta = FORD_BP_LATERAL_JERK / (jerk_speed * jerk_speed) *
|
||||
(SAFETY_MIN(elapsed, FORD_BP_STEER_PERIOD_US) * 0.000001F) * FORD_BP_GAIN_MIN;
|
||||
valid &= SAFETY_ABS(equivalent - ford_bp_equivalent_last) <= (jerk_delta + FORD_BP_SHADOW_ROUNDING_MARGIN);
|
||||
if (ford_bp_control_seen) { valid &= elapsed >= 40000U; }
|
||||
} else {
|
||||
valid &= path == 0;
|
||||
}
|
||||
if (valid) {
|
||||
ford_bp_rt_msgs += 1U;
|
||||
ford_bp_equivalent_last = active ? equivalent : 0.0F;
|
||||
ford_bp_path_last = active ? path : 0;
|
||||
ford_bp_control_counter = (uint8_t)counter;
|
||||
ford_bp_control_counter_seen = true;
|
||||
ford_bp_control_seen = true;
|
||||
ford_bp_control_us = now;
|
||||
if (!active) { ford_bp_shadow_seen = false; }
|
||||
} else {
|
||||
ford_bp_invalidate_shadow();
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Hash-bound upstream angle execution and current-curvature controller oracle.
|
||||
|
||||
Only imports are removed from the upstream angle/human module AST; every other
|
||||
statement and function remains exact. Glue supplies current ABI types, source
|
||||
fixed defaults and the source's no-op lazy-init/yaw-default adapter. No BluePilot
|
||||
curvature strategy is loaded. Its diagnostic limiter cannot affect angle output.
|
||||
"""
|
||||
import ast
|
||||
from collections import namedtuple
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
from opendbc.car import structs, DT_CTRL
|
||||
from opendbc.car.ford.values import CAR, CarControllerParams
|
||||
from opendbc.car.ford.fordcan import CanBus
|
||||
from opendbc.car.lateral import AngleSteeringLimits, apply_std_steer_angle_limits
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
ROOT = Path(__file__).with_name("reference")
|
||||
MANIFEST = json.loads((ROOT / "manifest.json").read_text())
|
||||
|
||||
|
||||
def source(name):
|
||||
raw = (ROOT / (name + ".py.txt")).read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest() != MANIFEST[name]["sha256"]:
|
||||
raise AssertionError("Reference source differs from its pinned manifest: " + name)
|
||||
return ast.parse(raw.decode(), filename=MANIFEST[name]["origin"])
|
||||
|
||||
|
||||
def execute(tree, namespace, *, names=None):
|
||||
body = []
|
||||
for node in tree.body:
|
||||
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||
continue
|
||||
if names is not None:
|
||||
assigned = [target.id for target in node.targets if isinstance(target, ast.Name)] if isinstance(node, ast.Assign) else []
|
||||
if getattr(node, "name", None) not in names and not any(name in names for name in assigned):
|
||||
continue
|
||||
body.append(node)
|
||||
exec(compile(ast.Module(body=body, type_ignores=[]), "<pinned source>", "exec"), namespace)
|
||||
|
||||
|
||||
def angle():
|
||||
LateralResult = namedtuple("LateralResult", "apply_curvature curvature_rate path_offset path_angle ramp_type precision_type lateralUncertainty")
|
||||
namespace = {"np": np, "clip": np.clip, "interp": np.interp, "DT_CTRL": DT_CTRL,
|
||||
"CAR": CAR, "CarControllerParams": CarControllerParams, "ModelConstants": ModelConstants,
|
||||
"LateralResult": LateralResult, "AngleSteeringLimits": AngleSteeringLimits,
|
||||
"apply_std_steer_angle_limits": apply_std_steer_angle_limits}
|
||||
execute(source("human"), namespace)
|
||||
execute(source("values"), namespace, names={"_BP_ANGLE_RATE_UP", "_BP_ANGLE_RATE_DOWN", "BP_ANGLE_LIMITS"})
|
||||
execute(source("angle"), namespace)
|
||||
return namespace["LateralAngleExt"]
|
||||
|
||||
|
||||
def builders():
|
||||
namespace = {"CanBus": CanBus, "structs": structs}
|
||||
execute(source("fordcan"), namespace, names={"calculate_lat_ctl2_checksum"})
|
||||
execute(source("builders"), namespace, names={"create_lka_msg", "create_lat_ctl_msg", "create_lat_ctl2_msg", "_BP_LKA_SHADOW_CURVATURE_SCALE"})
|
||||
return namespace
|
||||
|
||||
|
||||
def curvature_controller():
|
||||
# Full exact pre-angle production controller, with unchanged current imports.
|
||||
namespace = {"__name__": "ford_curvature_pre_angle_reference"}
|
||||
exec(compile(source("starpilot_curvature_controller"), "<current pre-angle controller>", "exec"), namespace)
|
||||
return namespace["CarController"]
|
||||
|
||||
|
||||
class Angle:
|
||||
def __init__(self, identity):
|
||||
self.controller = angle()()
|
||||
self.controller.CP = SimpleNamespace(carFingerprint=identity)
|
||||
self.controller._ensure_lateral_curv_initialized = lambda _cp: None # Exact source guard is a no-op.
|
||||
self.controller.get_current_curvature = lambda cs: -cs.out.yawRate / max(cs.out.vEgoRaw, .1)
|
||||
self.controller.path_angle_last = 0.
|
||||
self.controller.lp = None # Read but unused by the source angle strategy.
|
||||
self.controller.lane_change_factor_bp = [0., 50.]
|
||||
self.controller.lane_change_factor_low = 1.
|
||||
self.controller.model = None
|
||||
self.controller.update_angle_params(None)
|
||||
|
||||
def step(self, inputs):
|
||||
c = self.controller
|
||||
c.sm = {"liveDelay": SimpleNamespace(lateralDelay=inputs.lateral_delay)}
|
||||
c.model = SimpleNamespace(orientationRate=SimpleNamespace(z=list(inputs.yaw_predictions)),
|
||||
meta=SimpleNamespace(laneChangeState=1 if inputs.lane_change else 0,
|
||||
laneChangeDirection=inputs.lane_direction)) if inputs.yaw_predictions else None
|
||||
cc = SimpleNamespace(latActive=inputs.active)
|
||||
cs = SimpleNamespace(out=SimpleNamespace(vEgoRaw=inputs.speed, yawRate=-inputs.measured_curvature * inputs.speed,
|
||||
steeringPressed=inputs.steering_pressed, steeringAngleDeg=inputs.steering_angle),
|
||||
lat_ctl_lim_stat=inputs.limit_status)
|
||||
return c.update_angle_strategy(cc, cs, SimpleNamespace(curvature=inputs.desired_curvature), c.CP)
|
||||
|
||||
|
||||
def trim():
|
||||
namespace = {"np": np, "interp": np.interp}
|
||||
execute(source("bpdev_trim"), namespace)
|
||||
return namespace["LaneCenterTrim"]
|
||||
@@ -0,0 +1,577 @@
|
||||
"""
|
||||
BluePilot: Ford CAN-FD path-angle–primary lateral control (developer).
|
||||
|
||||
Steering intent is c1 from κ → θ. Converts planner/model curvature into ``path_angle`` using
|
||||
PSCM short lookahead d_ref and y ≈ ½κ x² ⇒ path_angle = ½ κ d_ref (see
|
||||
``bluepilot/agent_info/20_FORD_PSCM_KNOWLEDGE_PACK.md``). Predicted curvature (modelV2) is
|
||||
blended with ``actuators.curvature`` per ``FordPathAngleBlendRatio`` (0 = planner only,
|
||||
1 = model only).
|
||||
|
||||
**c0 (path_offset) is always zero on the wire, unconditionally.** Angle mode has no centering
|
||||
trim -- an earlier port attempt piped a small additive trim onto path_angle through the
|
||||
curv-mode ``LC_PID_controller``, but it never actually tracked lane center correctly in this
|
||||
mode and was removed; only the DBC-required zero c0 remains.
|
||||
|
||||
**Human-turn override**: while the driver manually turns (same sustained-press + angle criteria
|
||||
as ``lateral_curv_ext``, via the shared ``HumanTurnDetector``), lateral is forced inactive (mode
|
||||
0, all-zero signals) instead of winding path_angle into a stale command the PSCM has to reconcile
|
||||
on release -- on the Mach-E's PSCM that reconciliation cost 2-3 s of dead time before control
|
||||
resumed. Mode 0 is panda-clean by construction: every ford.h check has a legitimate
|
||||
!steer_control_enabled branch, so no reset-bypass latch involvement. On release, path_angle ramps
|
||||
back in from zero through the soft ROC below (no jump seed) -- generous at human-turn speeds, and
|
||||
admitted by ford.h's path_angle ROC check (2% looser) without any bypass.
|
||||
"""
|
||||
import numpy as np
|
||||
from numpy import clip, interp
|
||||
|
||||
from opendbc.car import DT_CTRL
|
||||
from opendbc.car.lateral import apply_std_steer_angle_limits
|
||||
from opendbc.car.ford.values import CAR, CarControllerParams
|
||||
from opendbc.sunnypilot.car.ford.lateral_curv_ext import LateralResult
|
||||
from opendbc.sunnypilot.car.ford.human_turn import HumanTurnDetector
|
||||
from opendbc.sunnypilot.car.ford.values_ext import BP_ANGLE_LIMITS
|
||||
from selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
# Hard-coded per-platform gain defaults.
|
||||
# CAN vehicles (Escape MK4, Bronco Sport, Explorer, Maverick, Edge)
|
||||
_GAIN_CAN = (1.00, 1.15)
|
||||
# CAN-FD body-on-frame trucks (F-150, Lightning, Expedition, Ranger)
|
||||
_GAIN_CANFD_BOF = (0.95, 0.95)
|
||||
# CAN-FD unibody SUVs (Mustang Mach-E, Escape MK4.5)
|
||||
_GAIN_CANFD_SUV = (1.00, 1.05)
|
||||
|
||||
_CANFD_BOF_CARS = frozenset({
|
||||
CAR.FORD_F_150_MK14,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1,
|
||||
CAR.FORD_EXPEDITION_MK4,
|
||||
CAR.FORD_RANGER_MK2,
|
||||
})
|
||||
_CANFD_SUV_CARS = frozenset({
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1,
|
||||
CAR.FORD_ESCAPE_MK4_5,
|
||||
})
|
||||
|
||||
|
||||
# DBC ``LatCtlPath_An_Actl`` (rad) — panda safety uses the same in ``ford.h``; PSCM enforces in firmware.
|
||||
FORD_DBC_PATH_ANGLE_MIN = -0.5
|
||||
FORD_DBC_PATH_ANGLE_MAX = 0.5235
|
||||
|
||||
|
||||
# PSCM d_ref (m) vs speed (m/s) — 6 points; above ~55.6 m/s use plateau + optional cap to 5 m.
|
||||
_PSCM_DREF_SPEEDS_MS = (0.0, 4.17, 27.78, 41.67, 50.0, 55.56)
|
||||
_PSCM_DREF_M = (0.5, 0.95, 1.4, 2.075, 2.75, 3.875)
|
||||
|
||||
# Default blend ratio validated on F-150 fleet data (0.5s lookup time).
|
||||
_FORD_PATH_ANGLE_BLEND_RATIO_DEFAULT = 0.50
|
||||
|
||||
# Variable lookup time (VLT): curvature_lookup_time adapts to speed and curvature magnitude.
|
||||
# t_lookup = t_base + t_extra_max × speed_factor(v) × kappa_factor(|κ|)
|
||||
# t_base = liveDelay.lateralDelay + DT_MDL — always matches the planner's pre-compensation floor.
|
||||
# Extra lookahead collapses toward zero at high speed (PSCM responds faster)
|
||||
# and at large curvature (prevents blend importing a "start unwinding" signal too early).
|
||||
_DT_MDL = 0.05 # model loop period (matches common/realtime.py)
|
||||
_VLT_T_EXTRA_MAX = 0.10 # max extra lookahead above t_base
|
||||
_VLT_V_LOW_MS = 25.0 * 0.44704 # 25 mph — full extra lookahead at or below this speed
|
||||
_VLT_V_HIGH_MS = 55.0 * 0.44704 # 55 mph — no extra lookahead at or above this speed
|
||||
_VLT_KAPPA_FULL = 0.005 # 1/m — full extra lookahead below this curvature (200m+ radius)
|
||||
_VLT_KAPPA_TAPER = 0.020 # 1/m — no extra lookahead above this curvature (50m radius)
|
||||
|
||||
# Rate cap on path_angle magnitude DECREASE during PSCM LimitReached (rad/call = 0.40 rad/s).
|
||||
# Both model and planner naturally drop path_angle ~0.36 rad/s at a sharp 90° apex, while the PSCM is
|
||||
# physically pinned and cannot execute the rapidly falling desired angle. The resulting actual-vs-desired
|
||||
# gap (up to 47° observed) causes a snap correction the moment the PSCM is released. This cap limits
|
||||
# the desired-angle drop rate to what the PSCM can reasonably track, at the cost of holding the car
|
||||
# slightly more in the curve during saturation.
|
||||
# BluePilot: this strategy runs once per STEER_STEP (CarControllerParams.STEER_STEP=5), i.e. once
|
||||
# every 5th 100Hz control tick = 20Hz, not every tick. The original 0.004 rad/call value (and its
|
||||
# "50Hz" comment, corrected above) was authored 2026-05-11 on bp-sid-simple, which had already
|
||||
# switched STEER_STEP 5->1 (true 100Hz) on 2026-04-22 -- so it was tuned at 100Hz real cadence
|
||||
# even though its own comment mistakenly said 50Hz. Scaled x5 here to restore the same real-world
|
||||
# 0.40 rad/s (23 deg/s) unwind rate on this branch's actual 20Hz cadence.
|
||||
_PSCM_SAT_UNWIND_RATE = 0.02 # rad/call (0.02 * 20Hz = 0.40 rad/s)
|
||||
|
||||
# Post-override stall blip. Road test 2026-07-14 (route 886240741b067740/000000bd--feb980680f)
|
||||
# showed that after driver-touch episodes the Mach-E PSCM keeps reporting InProgress but honors
|
||||
# path_angle at only ~0.56x (healthy hands-free delivery on the same route: ~0.95 median). The
|
||||
# current-curvature deviation clip below then pins kappa_cmd at measured + CURVATURE_ERROR, so the
|
||||
# command can never lead the car enough to overcome the attenuation -- a stall equilibrium the
|
||||
# driver reads as "not engaging" (wire path_angle flat at ~4 deg for 4.5 s while desired kappa
|
||||
# climbed to 3x measured, EPS motor current ~0 A). A short mode-0 pulse -- the identical
|
||||
# panda-clean wire pattern the human-turn override sends, no ford.h involvement -- resets the
|
||||
# PSCM's authority, after which path_angle ramps back in from zero through the soft ROC.
|
||||
_STEER_DT = CarControllerParams.STEER_STEP * DT_CTRL # 20 Hz lateral tick (matches human_turn.py)
|
||||
_STALL_GAP_MIN = 2.0 * CarControllerParams.CURVATURE_ERROR # desired must lead measured by 2x the clip tolerance
|
||||
_STALL_HOLD_S = 0.5 # accumulated clip-binding time before a pulse fires
|
||||
_STALL_BLIP_FRAMES = 6 # mode-0 pulse length (6 frames @ 20 Hz = 300 ms; PSCM acked mode 0 in ~150 ms on-road)
|
||||
_STALL_COOLDOWN_S = 2.0 # re-arm delay after a pulse (release ramp + PSCM response time)
|
||||
_STALL_MAX_BLIPS = 3 # give up on a stuck episode; devLim telemetry keeps recording the stall
|
||||
# Proactive hand-off blip: any sustained driver press attenuates the PSCM (route 000000be seg 4:
|
||||
# 3 s of sub-45-deg circle-exit steering left it at ~0x delivery, and the reactive detector's
|
||||
# fire-after-the-stall-develops timing meant 2.4 s of dead-straight running into the next curve
|
||||
# before the pulse landed). Firing the same pulse on the falling edge of a sustained press resets
|
||||
# the PSCM while the car is straight and the command is small -- a 300 ms lateral gap right at
|
||||
# hand-off, imperceptible, instead of a missed curve. The reactive detector above stays as backstop.
|
||||
_PRESS_BLIP_MIN_S = 0.5 # press must last this long before its release earns a pulse
|
||||
# The pulse releases steering for 300 ms; never fire it in a curve.
|
||||
_BLIP_MAX_PATH_ANGLE = 0.10 # rad
|
||||
|
||||
|
||||
def pscm_d_ref_m(v_ego_ms: float) -> float:
|
||||
v = max(float(v_ego_ms), 0.0)
|
||||
d = float(np.interp(v, _PSCM_DREF_SPEEDS_MS, _PSCM_DREF_M))
|
||||
if v > _PSCM_DREF_SPEEDS_MS[-1]:
|
||||
# Doc: d_ref table ends at 3.875 m; contribution saturates for high speed — cap at 5 m.
|
||||
d = min(5.0, d)
|
||||
return d
|
||||
|
||||
|
||||
class LateralAngleExt:
|
||||
def __init__(self, CP=None, CP_SP=None):
|
||||
# Predicted-curvature blend for path_angle: pred * b + desired * (1-b); b from ``FordPathAngleBlendRatio``
|
||||
self.path_angle_blend_ratio = _FORD_PATH_ANGLE_BLEND_RATIO_DEFAULT
|
||||
# Max extra VLT above t_base; from ``FordVLTExtraMax`` param
|
||||
self.vlt_extra_max = _VLT_T_EXTRA_MAX
|
||||
# Telemetry: final path_angle (rad) after limits (see bp_card_publisher)
|
||||
self.bp_path_angle_final = 0.0
|
||||
# High-speed gain factors: set per-platform via carFingerprint in update_angle_params.
|
||||
self.path_angle_gain_lowC_highV = 1.0 # dampening at high speed, low curvature
|
||||
self.path_angle_gain_highC_highV = 1.0 # gain at high speed, high curvature
|
||||
self.bp_path_angle_gain_lowC_highV = 1.0
|
||||
self.bp_path_angle_gain_highC_highV = 1.0
|
||||
# User-tunable "feel" multipliers: read from the angle-tuning Params below.
|
||||
self.low_speed_curv_factor = 1.0
|
||||
self.high_speed_curv_factor = 1.0
|
||||
self.user_dampening_factor = 1.0
|
||||
self.bp_low_speed_curv_factor = 1.0
|
||||
self.bp_high_speed_curv_factor = 1.0
|
||||
# BluePilot: angle mode's own lane-change scaling factor, independent of curvature mode's
|
||||
# lane_change_factor_high_curv -- angle needs a boost (>1) where curvature needs a cut (<1).
|
||||
self.lane_change_factor_high_ang = 1.0
|
||||
# Telemetry: variable curvature lookup time used this frame (s)
|
||||
self.bp_curvature_lookup_time = _VLT_T_EXTRA_MAX + 0.3725 # warm start at ~0.5s
|
||||
# BluePilot: error-clipped kappa path_angle was derived from -- carcontroller.py reads this as
|
||||
# shadow_curvature for ford.h's angle-mode deviation check. Actively consumed, not telemetry.
|
||||
self.bp_kappa_cmd = 0.0
|
||||
# BluePilot: rate-limit diagnostics (controllerStateBP)
|
||||
self.bp_angle_rate_limited = False # path_angle soft-ROC clip actually bit this frame
|
||||
self.bp_curvature_rate_limited = False # equivalent curvature would be rate-limited by curv-mode logic (sim)
|
||||
self.bp_curvature_deviation_limited = False # current_curvature error-clip constrained kappa_cmd this frame
|
||||
self.sim_curvature_last = 0.0 # shadow curvature-mode last for the curvatureRateLimited sim
|
||||
# Exit detection: track previous desired curvature to sense when planner is actively reducing
|
||||
self._desired_curvature_last = 0.0
|
||||
# Human-turn override: while the driver manually turns, lateral is forced inactive (mode 0,
|
||||
# all-zero signals) instead of winding path_angle into a stale command the PSCM can't cleanly
|
||||
# reconcile on release (2-3 s re-engage dead time observed on Mach-E). See module docstring.
|
||||
# Note: in CarController this attribute is shared with LateralCurvExt (same mixin instance) --
|
||||
# only one lateral strategy runs per frame, so a single detector serves both.
|
||||
self.human_turn_detector = HumanTurnDetector()
|
||||
self.angle_human_turn_active = False # read by carcontroller to force mode 0
|
||||
# Post-override stall blip state (see module constants). angle_stall_blip_active is read by
|
||||
# carcontroller to force mode 0, exactly like angle_human_turn_active.
|
||||
self.stall_blip_hold_s = 0.0 # accumulated deviation-clip-binding time toward a pulse
|
||||
self.stall_blip_frames_left = 0 # remaining pulse frames; > 0 -> mode 0 on the wire
|
||||
self.stall_blip_cooldown_s = 0.0 # re-arm delay after a pulse
|
||||
self.stall_blip_count = 0 # pulses fired this stall episode
|
||||
self.angle_stall_blip_active = False
|
||||
self.press_timer_s = 0.0 # continuous steeringPressed time, for the hand-off blip
|
||||
|
||||
def update_angle_params(self, params):
|
||||
"""Sets per-platform gain defaults and reads user angle-tuning params."""
|
||||
self._ensure_lateral_curv_initialized(self.CP)
|
||||
fp = getattr(self.CP, 'carFingerprint', '')
|
||||
if fp in _CANFD_BOF_CARS:
|
||||
low, high = _GAIN_CANFD_BOF
|
||||
elif fp in _CANFD_SUV_CARS:
|
||||
low, high = _GAIN_CANFD_SUV
|
||||
else:
|
||||
low, high = _GAIN_CAN
|
||||
self.path_angle_gain_lowC_highV = low
|
||||
self.path_angle_gain_highC_highV = high
|
||||
if params is not None and hasattr(params, "get"):
|
||||
for attr, key, min_value, max_value in (
|
||||
("low_speed_curv_factor", "FordLowSpeedFactor_ang", 0.5, 1.5),
|
||||
("high_speed_curv_factor", "FordHighSpeedFactor_ang", 0.5, 1.5),
|
||||
("user_dampening_factor", "FordHighSpeedDampening_ang", 0.25, 1.25),
|
||||
):
|
||||
try:
|
||||
raw = params.get(key, return_default=True)
|
||||
if raw is not None and raw != b"":
|
||||
setattr(self, attr, float(clip(
|
||||
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), min_value, max_value)))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
raw = params.get("lane_change_factor_high_ang", return_default=True)
|
||||
if raw is not None and raw != b"":
|
||||
self.lane_change_factor_high_ang = float(clip(
|
||||
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), 0.85, 1.50))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def update_angle_strategy(self, CC, CS, actuators, CP):
|
||||
"""
|
||||
Curvature from planner (+ optional predicted blend) → path_angle via ½·κ·d_ref.
|
||||
c0 (path_offset) is always zero on the wire -- no centering trim in angle mode. c2 and c3 are zero.
|
||||
Blended κ is not passed through Ford c2 rate / DBC limits (those target the curvature actuator).
|
||||
"""
|
||||
self._ensure_lateral_curv_initialized(CP)
|
||||
|
||||
v_ego = float(CS.out.vEgoRaw)
|
||||
d_ref = pscm_d_ref_m(v_ego)
|
||||
|
||||
curvature_rate = 0.0
|
||||
path_offset = 0.0
|
||||
path_angle = 0.0
|
||||
ramp_type = 0
|
||||
lateral_uncertainty = 0.0
|
||||
precision = 1
|
||||
|
||||
if not CC.latActive:
|
||||
self.path_angle_last = 0.0
|
||||
self.bp_path_angle_final = 0.0
|
||||
self.apply_curvature_last = 0.0
|
||||
self.bp_angle_rate_limited = False
|
||||
self.bp_curvature_rate_limited = False
|
||||
self.bp_curvature_deviation_limited = False
|
||||
self.sim_curvature_last = 0.0
|
||||
# Publish the shadow curvature from the measured curvature while inactive. LKA keeps
|
||||
# carrying angle_mode_engaged whenever angle mode is configured (independent of
|
||||
# latActive), and ford.h latches the shadow from every LKA frame -- so the latched
|
||||
# value must track reality here, not sit at a stale zero. Otherwise the first enabled
|
||||
# LMC frame after (re-)engage races LKA's 33Hz latch against LMC's 20Hz enable bit and
|
||||
# ford.h's deviation check compares a zero shadow against real measured curvature.
|
||||
# (ford.h skips the check while steer_control_enabled is 0, so the value is free to
|
||||
# follow the measurement during the inactive period itself.)
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS)
|
||||
self.human_turn_detector.reset()
|
||||
self.angle_human_turn_active = False
|
||||
self.stall_blip_hold_s = 0.0
|
||||
self.stall_blip_frames_left = 0
|
||||
self.stall_blip_cooldown_s = 0.0
|
||||
self.stall_blip_count = 0
|
||||
self.angle_stall_blip_active = False
|
||||
self.press_timer_s = 0.0
|
||||
self.precision_type = 1
|
||||
return LateralResult(
|
||||
apply_curvature=0.0,
|
||||
curvature_rate=0.0,
|
||||
path_offset=0.0,
|
||||
path_angle=0.0,
|
||||
ramp_type=0,
|
||||
precision_type=1,
|
||||
lateralUncertainty=0.0,
|
||||
)
|
||||
|
||||
# Human-turn override: sustained driver press + large wheel angle → force lateral inactive
|
||||
# (carcontroller drops mode to 0; all signals are zero on the wire) so path_angle can't wind
|
||||
# into a stale command while the driver turns. Always on in angle mode (no param gate) -- the
|
||||
# curv-suffixed human-turn toggle belongs to curvature mode's reset strategy, and the Mach-E
|
||||
# PSCM re-engage stall this prevents is not something a user should be able to opt out of.
|
||||
# On release, no jump seed: path_angle_last is 0, so the normal flow below ramps the command
|
||||
# back in through the soft ROC -- generous at human-turn speeds, no panda bypass involved.
|
||||
self.angle_human_turn_active = self.human_turn_detector.update(
|
||||
True, CS.out.steeringPressed, CS.out.steeringAngleDeg)
|
||||
if self.angle_human_turn_active:
|
||||
self.path_angle_last = 0.0
|
||||
self.bp_path_angle_final = 0.0
|
||||
self.apply_curvature_last = 0.0
|
||||
self.bp_angle_rate_limited = False
|
||||
self.bp_curvature_rate_limited = False
|
||||
self.bp_curvature_deviation_limited = False
|
||||
self.sim_curvature_last = 0.0
|
||||
# Truthful shadow during the override (mirrors the inactive path -- see the comment
|
||||
# there): the driver is steering, so the honest command is the car's actual curvature,
|
||||
# and the panda-latched shadow stays current for the re-engage frame.
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS)
|
||||
# Keep exit detection current so resume doesn't compare against a stale pre-turn value.
|
||||
self._desired_curvature_last = float(actuators.curvature)
|
||||
# A human turn ends any stall episode -- its own mode 0 does the PSCM reset job. That also
|
||||
# covers the press so far: only press time accumulated AFTER the latch releases should earn
|
||||
# a hand-off pulse.
|
||||
self.stall_blip_hold_s = 0.0
|
||||
self.stall_blip_frames_left = 0
|
||||
self.stall_blip_cooldown_s = 0.0
|
||||
self.stall_blip_count = 0
|
||||
self.angle_stall_blip_active = False
|
||||
self.press_timer_s = 0.0
|
||||
self.precision_type = 1
|
||||
return LateralResult(
|
||||
apply_curvature=0.0,
|
||||
curvature_rate=0.0,
|
||||
path_offset=0.0,
|
||||
path_angle=0.0,
|
||||
ramp_type=0,
|
||||
precision_type=1,
|
||||
lateralUncertainty=0.0,
|
||||
)
|
||||
|
||||
# Proactive hand-off blip: the falling edge of a sustained press earns an immediate mode-0
|
||||
# pulse (see _PRESS_BLIP_MIN_S) -- resets the PSCM's press-induced attenuation right at
|
||||
# hand-off, while the car is straight and the command small, instead of waiting for the
|
||||
# reactive stall detector below to watch the car miss the next curve first.
|
||||
if CS.out.steeringPressed:
|
||||
self.press_timer_s += _STEER_DT
|
||||
else:
|
||||
if (self.press_timer_s >= _PRESS_BLIP_MIN_S and self.stall_blip_cooldown_s <= 0.0
|
||||
and self.stall_blip_frames_left <= 0
|
||||
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
|
||||
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
|
||||
self.press_timer_s = 0.0
|
||||
|
||||
# Stall-blip pulse in progress: hold lateral inactive (mode 0, all-zero signals -- the same
|
||||
# wire pattern as the human-turn override, no ford.h involvement) for _STALL_BLIP_FRAMES so the
|
||||
# PSCM drops its post-override attenuation, then release; path_angle ramps back in from zero
|
||||
# through the soft ROC exactly like a human-turn release. Detection lives at the end of the
|
||||
# normal flow below.
|
||||
if self.stall_blip_frames_left > 0:
|
||||
self.stall_blip_frames_left -= 1
|
||||
self.angle_stall_blip_active = True
|
||||
self.path_angle_last = 0.0
|
||||
self.bp_path_angle_final = 0.0
|
||||
self.apply_curvature_last = 0.0
|
||||
self.bp_angle_rate_limited = False
|
||||
self.bp_curvature_rate_limited = False
|
||||
self.bp_curvature_deviation_limited = False
|
||||
self.sim_curvature_last = 0.0
|
||||
# Truthful shadow during the blip (see the inactive-path comment).
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS)
|
||||
self._desired_curvature_last = float(actuators.curvature)
|
||||
self.precision_type = 1
|
||||
if self.stall_blip_frames_left <= 0:
|
||||
self.stall_blip_cooldown_s = _STALL_COOLDOWN_S
|
||||
return LateralResult(
|
||||
apply_curvature=0.0,
|
||||
curvature_rate=0.0,
|
||||
path_offset=0.0,
|
||||
path_angle=0.0,
|
||||
ramp_type=0,
|
||||
precision_type=1,
|
||||
lateralUncertainty=0.0,
|
||||
)
|
||||
self.angle_stall_blip_active = False
|
||||
|
||||
self.precision_type = 1
|
||||
precision = 1
|
||||
LP = self.lp
|
||||
desired_curvature = float(actuators.curvature)
|
||||
|
||||
# Variable lookup time: t_base tracks planner pre-compensation; extra tapers on high speed and large curves.
|
||||
# Cap liveDelay at 0.15s for VLT purposes. liveDelay can calibrate up to ~420ms on some runs, which inflates
|
||||
# VLT to 0.6s and pushes the model lookahead 5m into the curve. At that depth the model sees full peak
|
||||
# curvature, kappa_entering stays True, and the exit-biased blend is permanently disabled — causing the car
|
||||
# to command max path_angle through the entire apex. 0.15s gives t_base ≤ 0.20s and VLT ≤ 0.33s, restoring
|
||||
# the 2.8m lookahead that kept kappa_entering False at the apex in successful earlier runs.
|
||||
_t_base = float(clip(self.sm['liveDelay'].lateralDelay, 0.1, 0.15)) + _DT_MDL
|
||||
_speed_factor = float(interp(v_ego, [_VLT_V_LOW_MS, _VLT_V_HIGH_MS], [1.0, 0.0]))
|
||||
# Direction-aware kappa factor: on curve ENTRY (model shows more curvature at t_base than planner now),
|
||||
# keep full lookahead so pre-steering begins early. On exit/apex, taper by magnitude to prevent unwind.
|
||||
_kappa_at_t_base = 0.0
|
||||
if self.model is not None and len(self.model.orientationRate.z) >= 17:
|
||||
_curvatures_ref = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
|
||||
_kappa_at_t_base = abs(float(interp(_t_base, ModelConstants.T_IDXS, _curvatures_ref)))
|
||||
_kappa_entering = _kappa_at_t_base > abs(desired_curvature)
|
||||
if _kappa_entering:
|
||||
_kappa_factor = 1.0 # curve deepening ahead: full extra lookahead for gradual entry
|
||||
else:
|
||||
_kappa_factor = float(interp(abs(desired_curvature), [_VLT_KAPPA_FULL, _VLT_KAPPA_TAPER], [1.0, 0.0]))
|
||||
curvature_lookup_time = _t_base + self.vlt_extra_max * _speed_factor * _kappa_factor
|
||||
self.bp_curvature_lookup_time = curvature_lookup_time
|
||||
|
||||
predicted_curvature = 0.0
|
||||
if self.model is not None and len(self.model.orientationRate.z) >= 17:
|
||||
curvatures = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
|
||||
predicted_curvature = float(
|
||||
interp(curvature_lookup_time, ModelConstants.T_IDXS, curvatures)
|
||||
)
|
||||
|
||||
b = float(self.path_angle_blend_ratio)
|
||||
b = float(clip(b, 0.0, 1.0))
|
||||
|
||||
# Exit-biased blend: near the PSCM authority limit or while the planner is actively
|
||||
# reducing curvature (exit detected), drop model prediction weight from 60% → ~15%.
|
||||
# This lets the planner's natural unwind dominate instead of being diluted by a model
|
||||
# prediction that still sees the curve (→ seg-14 slow unwind) or that snaps when its
|
||||
# lookahead window crosses the curve exit (→ seg-17 snap + reverse PSCM hit).
|
||||
# Normal gentle curves are unaffected: no PSCM limit, no falling desired → full b=0.60.
|
||||
_pscm_lim = getattr(CS, 'lat_ctl_lim_stat', 0)
|
||||
# In angle mode, LatCtlLim_D_Stat (→ lat_ctl_lim_stat) does not fire.
|
||||
# Previously used angleState.saturated (CtrSat) as a proxy, but CtrSat fires whenever the car
|
||||
# lags the commanded path_angle by > 2.5° — which happens during any normal curve entry.
|
||||
# That caused a positive-feedback flat-line: under-steer → CtrSat → path_angle frozen → more under-steer.
|
||||
# Use DBC-limit proximity instead: only block when path_angle is already near the ±0.5 rad CAN limits,
|
||||
# which is the only condition where the anti-snap unwind rate cap makes physical sense.
|
||||
_dbc_sat = (self.path_angle_last >= FORD_DBC_PATH_ANGLE_MAX * 0.90 or
|
||||
self.path_angle_last <= FORD_DBC_PATH_ANGLE_MIN * 0.90)
|
||||
_in_hard_sat = _pscm_lim >= 2 or _dbc_sat
|
||||
# BluePilot: per-call delta threshold. The original 0.002 was authored 2026-05-07 on
|
||||
# bp-sid-simple (9c3d000fd), which ran STEER_STEP=1 (true 100Hz, switched 2026-04-22) -- so it
|
||||
# was tuned to trigger on planner unwind faster than 0.2 (1/m)/s. Scaled x5 here to restore
|
||||
# that same real-world trigger rate on this branch's actual 20Hz cadence; unscaled it fired at
|
||||
# 0.04 (1/m)/s, collapsing the model blend on mild straightening instead of genuine exits.
|
||||
# Same bug class and fix as _PSCM_SAT_UNWIND_RATE and _soft_roc above.
|
||||
_desired_falling = abs(desired_curvature) < abs(self._desired_curvature_last) - 0.010
|
||||
_on_exit_near_limit = not _kappa_entering and (_pscm_lim >= 1 or _in_hard_sat or _desired_falling)
|
||||
b_blend = float(clip(b * 0.25, 0.0, 1.0)) if _on_exit_near_limit else b
|
||||
requested_curvature = predicted_curvature * b_blend + desired_curvature * (1.0 - b_blend)
|
||||
self._desired_curvature_last = desired_curvature
|
||||
|
||||
if self.model is not None:
|
||||
self.lane_change = self.model.meta.laneChangeState in (1, 2, 3)
|
||||
else:
|
||||
self.lane_change = False
|
||||
|
||||
lane_change_factor = interp(
|
||||
v_ego, self.lane_change_factor_bp, [self.lane_change_factor_low, self.lane_change_factor_high_ang]
|
||||
)
|
||||
if self.lane_change and self.model is not None:
|
||||
if self.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
|
||||
requested_curvature *= lane_change_factor
|
||||
precision = 0
|
||||
elif self.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
|
||||
requested_curvature *= lane_change_factor
|
||||
precision = 0
|
||||
self.precision_type = precision
|
||||
|
||||
# Use planner / predicted κ directly for the κ → path_angle map; we are not sending κ on CAN.
|
||||
kappa_cmd = float(requested_curvature)
|
||||
|
||||
# BluePilot: clip kappa_cmd to current_curvature (measured, from yaw rate) +- CURVATURE_ERROR,
|
||||
# mirroring lateral_curv_ext.py's apply_ford_curvature_limits_ext exactly (same formula, same
|
||||
# v_ego > 9 gate, same CarControllerParams.CURVATURE_ERROR tolerance). Without this, kappa_cmd
|
||||
# (and therefore path_angle, and the shadow_curvature sent to ford.h) can legitimately lead the
|
||||
# measured curvature by more than ford.h's angle-error tolerance during normal curve entry/exit
|
||||
# -- the shadow-curvature deviation check (ford_shadow_curvature_error_check) would then block
|
||||
# routinely, not just on genuine pothole/override divergence. Curvature mode has always clipped
|
||||
# here; this brings angle mode's actual steering intent in line with that proven behavior rather
|
||||
# than only clipping the value reported to panda (which would make the check a no-op).
|
||||
current_curvature = self.get_current_curvature(CS)
|
||||
self.bp_curvature_deviation_limited = False
|
||||
if v_ego > 9:
|
||||
_kappa_cmd_pre_error_clip = kappa_cmd
|
||||
kappa_cmd = float(clip(kappa_cmd, current_curvature - CarControllerParams.CURVATURE_ERROR,
|
||||
current_curvature + CarControllerParams.CURVATURE_ERROR))
|
||||
# BluePilot: did this clip actually constrain kappa_cmd this frame (deviation from measured,
|
||||
# not rate-of-change -- see carcontroller.py)?
|
||||
self.bp_curvature_deviation_limited = bool(abs(kappa_cmd - _kappa_cmd_pre_error_clip) > 1e-9)
|
||||
|
||||
lateral_uncertainty = 0.0 # no curvature-limit ladder until angle-mode torque display is defined
|
||||
|
||||
|
||||
|
||||
# Speed-interpolated gain: at low speed both curves use 1.0; at high speed the params take effect.
|
||||
self.low_gain_calc = interp(
|
||||
v_ego, [13.5, 26.82], [1.0, (self.path_angle_gain_lowC_highV * self.user_dampening_factor)]
|
||||
)
|
||||
self.high_gain_calc = interp(v_ego, [13.5, 26.82], [(1.30 * self.low_speed_curv_factor), (self.path_angle_gain_highC_highV * self.high_speed_curv_factor)])
|
||||
|
||||
# As the curve gets bigger, we will need a little boost to the signal to to not understeer
|
||||
self.curvature_factor = interp(abs(kappa_cmd), [0.0007, 0.001], [self.low_gain_calc, self.high_gain_calc])
|
||||
|
||||
path_angle_calc = kappa_cmd * v_ego * self.curvature_factor
|
||||
path_angle = path_angle_calc
|
||||
|
||||
|
||||
# PSCM authority limit clamp.
|
||||
# On CANFD Fords in angle mode, LatCtlLim_D_Stat does not fire, so _pscm_lim stays 0.
|
||||
# _in_hard_sat (computed above) combines _pscm_lim >= 2 with _dbc_sat (path_angle near ±0.5 rad limit).
|
||||
# LimitClose (_pscm_lim >= 1 only): block magnitude increases — exit-biased blend provides unwind.
|
||||
# Hard saturation (_in_hard_sat): block increases AND rate-limit decreases to _PSCM_SAT_UNWIND_RATE.
|
||||
# Without the decrease cap, model+planner drop path_angle at ~0.36 rad/s at a sharp apex,
|
||||
# driving desired steering 30°+ ahead of actual while the PSCM is pinned, causing a snap when released.
|
||||
if _in_hard_sat:
|
||||
_last = self.path_angle_last
|
||||
_last_mag = abs(_last)
|
||||
_curr_mag = abs(path_angle)
|
||||
if _curr_mag > _last_mag: # magnitude growing — block
|
||||
path_angle = _last
|
||||
elif _last_mag - _curr_mag > _PSCM_SAT_UNWIND_RATE: # decreasing too fast — rate-limit
|
||||
_limited_mag = _last_mag - _PSCM_SAT_UNWIND_RATE
|
||||
path_angle = float(_limited_mag if _last >= 0 else -_limited_mag)
|
||||
elif _pscm_lim >= 1: # LimitClose (F150/non-angle-mode only): block increases only
|
||||
path_angle = float(clip(path_angle, -abs(self.path_angle_last), abs(self.path_angle_last)))
|
||||
|
||||
path_angle = min(FORD_DBC_PATH_ANGLE_MAX, max(FORD_DBC_PATH_ANGLE_MIN, path_angle))
|
||||
|
||||
# Soft ROC limit — unconditional, slightly tighter than ford.h, applied before the
|
||||
# hardware bypass in ford.h is re-enabled. Lets us observe whether the limit would
|
||||
# suppress control and tune it, while the PSCM still receives the clipped value.
|
||||
# BluePilot: this strategy runs once per STEER_STEP (CarControllerParams.STEER_STEP=5), i.e.
|
||||
# once every 5th 100Hz control tick = 20Hz, not every tick -- ported "verbatim from bp-sid-simple"
|
||||
# (2026-06-13), which runs STEER_STEP=1 (true 100Hz, switched 2026-04-22). The y-values below are
|
||||
# scaled x5 from the original [0.011, 0.011, 0.0085, 0.0018] to restore the same real-world rate
|
||||
# (63/63/49/10 deg/s at v=9-10/15/25) on this branch's actual 20Hz cadence. See ford.h's
|
||||
# FORD_PATH_ANGLE_LIMITS, which must mirror this scaling (x1.02 looser) to stay a true backstop.
|
||||
_soft_roc = float(interp(v_ego, [9., 10., 15., 25.], [0.055, 0.055, 0.0425, 0.009]))
|
||||
_path_angle_pre_roc = path_angle
|
||||
path_angle = float(clip(path_angle,
|
||||
self.path_angle_last - _soft_roc,
|
||||
self.path_angle_last + _soft_roc))
|
||||
# BluePilot: did the soft ROC clip actually limit the path_angle we wanted to send this frame?
|
||||
self.bp_angle_rate_limited = bool(abs(path_angle - _path_angle_pre_roc) > 1e-9)
|
||||
|
||||
|
||||
# c0 always zero -- no centering trim in angle mode.
|
||||
path_offset = 0.0
|
||||
|
||||
# Telemetry / state
|
||||
self.bp_path_angle_gain_lowC_highV = self.path_angle_gain_lowC_highV
|
||||
self.bp_path_angle_gain_highC_highV = self.path_angle_gain_highC_highV
|
||||
self.bp_low_speed_curv_factor = self.low_speed_curv_factor
|
||||
self.bp_high_speed_curv_factor = self.high_speed_curv_factor
|
||||
self.path_angle_last = path_angle
|
||||
self.bp_path_angle_final = path_angle
|
||||
self.apply_curvature_last = 0.0
|
||||
# BluePilot: the error-clipped kappa path_angle was derived from -- carcontroller.py reads this
|
||||
# as shadow_curvature for ford.h's angle-mode deviation check (see fordcan_ext.create_lka_msg).
|
||||
# Not just telemetry: an actively-consumed value, unlike the removed *_kappa_cmd_raw stubs.
|
||||
# While the driver is pressing (before the human-turn override latches), the clipped planner
|
||||
# kappa can't follow the wheel: the driver moves the measured curvature faster than the
|
||||
# deviation clip tracks it, so the shadow can exit ford.h's error band mid-curve -- the one
|
||||
# in-drive lateral safety block observed across ~3h of replayed road-test routes was exactly
|
||||
# this (driver fighting a sustained curve with the mode still enabled). The honest command
|
||||
# during a press is the driver's actual curvature.
|
||||
self.bp_kappa_cmd = self.get_current_curvature(CS) if CS.out.steeringPressed else kappa_cmd
|
||||
|
||||
# BluePilot: would the equivalent curvature (kappa_cmd) have been rate-limited by curvature-mode's
|
||||
# ROC (apply_std_steer_angle_limits)? kappa_cmd is already error-clipped above (same clip
|
||||
# curvature mode applies), so only the rate-of-change portion remains to simulate here.
|
||||
_equiv_curv_rl = apply_std_steer_angle_limits(kappa_cmd, self.sim_curvature_last, v_ego,
|
||||
CS.out.steeringAngleDeg, CC.latActive, BP_ANGLE_LIMITS)
|
||||
self.bp_curvature_rate_limited = bool(abs(_equiv_curv_rl - kappa_cmd) > 1e-9)
|
||||
self.sim_curvature_last = float(_equiv_curv_rl)
|
||||
|
||||
# Post-override stall detection (mechanism in the module constants' comment). Fires the mode-0
|
||||
# blip when, hands-free, desired curvature has led measured by more than 2x the deviation
|
||||
# clip's tolerance while the clip was actually binding for _STALL_HOLD_S accumulated seconds.
|
||||
# devLim flickers mid-stall (~63% duty on the diagnosis route), so off frames hold the
|
||||
# accumulator rather than resetting it; a closed gap or driver press ends the episode.
|
||||
self.stall_blip_cooldown_s = max(0.0, self.stall_blip_cooldown_s - _STEER_DT)
|
||||
_stall_gap = desired_curvature - current_curvature
|
||||
_stalled = (not CS.out.steeringPressed and not self.lane_change and v_ego > 9.0
|
||||
and abs(_stall_gap) > _STALL_GAP_MIN
|
||||
and abs(desired_curvature) > abs(current_curvature))
|
||||
if _stalled:
|
||||
if self.bp_curvature_deviation_limited and self.stall_blip_cooldown_s <= 0.0:
|
||||
self.stall_blip_hold_s += _STEER_DT
|
||||
if (self.stall_blip_hold_s >= _STALL_HOLD_S and self.stall_blip_count < _STALL_MAX_BLIPS
|
||||
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
|
||||
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
|
||||
self.stall_blip_hold_s = 0.0
|
||||
self.stall_blip_count += 1
|
||||
else:
|
||||
self.stall_blip_hold_s = 0.0
|
||||
if CS.out.steeringPressed or abs(_stall_gap) < 0.5 * _STALL_GAP_MIN:
|
||||
self.stall_blip_count = 0 # episode over: the car is tracking again or the driver took it
|
||||
|
||||
ramp_type = 2
|
||||
|
||||
|
||||
return LateralResult(
|
||||
apply_curvature=0.0,
|
||||
curvature_rate=curvature_rate,
|
||||
path_offset=path_offset,
|
||||
path_angle=path_angle,
|
||||
ramp_type=ramp_type,
|
||||
precision_type=self.precision_type,
|
||||
lateralUncertainty=lateral_uncertainty,
|
||||
)
|
||||
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
BluePilot: angle-mode "advanced lane positioning" -- a curvature-domain trim toward true
|
||||
lane-line center (plus an optional user left/right bias), for use with path_angle-primary
|
||||
control (``lateral_angle_ext.py``).
|
||||
|
||||
Under curvature-primary control, lane positioning rode on a second DBC signal (path_angle as
|
||||
a heading trim, c3) layered on top of curvature (c1, the primary). In angle mode, path_angle
|
||||
IS the primary signal -- there is no separate trim slot on the wire. An earlier attempt ported
|
||||
the old PID controller as an additive delta on the *final* path_angle value and it never tracked
|
||||
lane center correctly: path_angle there is a derived quantity (``kappa_cmd * v_ego *
|
||||
curvature_factor``), so an additive trim in that domain has the wrong (inverted) speed-dependence
|
||||
for a lane-centering nudge, and it bypasses every limiter angle mode already applies to
|
||||
``kappa_cmd`` (deviation clip, PSCM-saturation clamp, DBC clip, soft ROC).
|
||||
|
||||
This class instead computes a small correction in the **curvature domain**, meant to be added to
|
||||
``kappa_cmd`` before any of those limiters run -- so the correction automatically inherits all of
|
||||
them, with no new safety-relevant code path. This mirrors where a GM-focused fork (StarPilot,
|
||||
u/jc01rho -- ``selfdrive/controls/lib/drive_helpers.py::LaneCenteringController`` on
|
||||
``feat/lane-centering-camera-offset``) places their own lane-centering trim: on the universal
|
||||
``desired_curvature`` signal, upstream of any car-specific actuator conversion. The geometry here
|
||||
(``raw_correction = 2*error / lookahead**2``) is the same ``y ~= 1/2 * kappa * x**2`` relation
|
||||
BluePilot already uses for PSCM d_ref / path_angle elsewhere in this package, solved for the
|
||||
curvature that would eliminate a lateral position error at the lookahead distance.
|
||||
|
||||
**Model-position fallback (blend, not hard gate)**: mirrors ``lateral_curv_ext.py``'s
|
||||
``path_offset`` blend of ``path_offset_position`` (the model's own predicted path -- a "no
|
||||
correction" baseline) vs. ``path_offset_lanelines`` (the geometric lane center), weighted by a
|
||||
laneline confidence score. The *target* we correct toward is that same blend here, so:
|
||||
|
||||
- Good lane lines (confidence -> 1): target -> lane-line center + offset (full centering + bias).
|
||||
- No/poor lane lines (confidence -> 0): target -> the model's own current path + offset, i.e. the
|
||||
correction reduces to *just* the user's left/right bias riding on top of whatever the model
|
||||
already wants to do -- it never goes to zero just because lane lines dropped out. This is the
|
||||
case that matters on center-stripe-only roads with a curbed edge: there's no reliable lane line
|
||||
to center between, but the user's bias should still nudge the car off the curb.
|
||||
|
||||
Confidence uses the exact formula and breakpoints ``lateral_curv_ext.py`` already uses for
|
||||
``laneline_confidence`` / ``min_laneline_confidence_bp`` (smooth width-tolerance + laneline probs,
|
||||
not a hard cutoff) -- reusing an already-tuned, production formula instead of inventing a second one.
|
||||
"""
|
||||
import numpy as np
|
||||
from numpy import interp
|
||||
|
||||
# Laneline confidence blend -- ported verbatim from lateral_curv_ext.py's path_offset blend
|
||||
# (laneline_width_tolerance / min_laneline_confidence_bp) so both control schemes agree on what
|
||||
# "good lane lines" means.
|
||||
_WIDTH_TOLERANCE_BP = (2.4, 2.8, 3.75, 4.25) # m - the floor is added from StarPilot
|
||||
_WIDTH_TOLERANCE_V = (0.0, 0.81, 0.81, 0.59) # Adds StarPilot's low edge
|
||||
_STD_TOLERANCE_BP = (0.3, 0.5) #0.3 is from StarPilot to define bad references
|
||||
_STD_TOLERANCE_V = (0.81, 0.0) #0.81 is a known good value and allows it to fade to zero as std increases
|
||||
_CONFIDENCE_BP = (0.6, 0.8)
|
||||
_CONFIDENCE_V = (0.0, 1.0)
|
||||
|
||||
# Speed ramp: reuses curvature-mode's already-tuned centering-authority envelope
|
||||
# (LC_PID_speed_bp/v in lateral_curv_ext.py) instead of inventing a new one.
|
||||
_SPEED_RAMP_BP = (0.0, 9.0, 15.0) # m/s
|
||||
_SPEED_RAMP_V = (0.0, 0.0, 1.0)
|
||||
|
||||
# Lookahead distance for the position-error sample, clipped to a sane range (m).
|
||||
_LOOKAHEAD_MIN_M = 8.0
|
||||
_LOOKAHEAD_MAX_M = 35.0
|
||||
|
||||
# Hard safety ceiling on the raw correction magnitude (1/m) -- fixed, not a "feel" knob, same
|
||||
# treatment as _PSCM_SAT_UNWIND_RATE / _soft_roc in lateral_angle_ext.py.
|
||||
_MAX_RAW_CORRECTION = 0.004
|
||||
|
||||
# First-order smoothing time constant (s) -- avoids abrupt jumps in the trim.
|
||||
_SMOOTH_TAU_S = 0.4
|
||||
|
||||
# Rate-of-change limit on the applied correction (1/m per 20 Hz tick), independent of the
|
||||
# smoothing filter above. The filter alone still has its *fastest* slew immediately after a
|
||||
# target jump -- its per-tick step is proportional to how far the target moved, so a large,
|
||||
# sudden confidence swing (e.g. crossing into/out of a too-wide merge lane, or lane lines
|
||||
# dropping out entirely while the model's own path and the laneline center disagree) can still
|
||||
# produce a big single-tick correction change even though it's technically "smoothed". This caps
|
||||
# that step explicitly, the same way path_angle's own soft ROC (lateral_angle_ext.py) and
|
||||
# curvature mode's LC_path_angle_ROC (lateral_curv_ext.py) rate-limit their outputs. At this
|
||||
# rate, crossing the full -_MAX_RAW_CORRECTION..+_MAX_RAW_CORRECTION span takes ~2.7s; a more
|
||||
# typical confidence-transition jump (a fraction of that) resolves proportionally faster.
|
||||
_CORRECTION_ROC_PER_TICK = 0.00015
|
||||
|
||||
|
||||
class LaneCenterTrim:
|
||||
def __init__(self):
|
||||
self._correction = 0.0
|
||||
|
||||
def reset(self) -> None:
|
||||
self._correction = 0.0
|
||||
|
||||
def update(self, kappa_cmd: float, model, v_ego: float, enabled: bool, offset: float,
|
||||
gain: float, lat_active: bool, lane_change: bool) -> float:
|
||||
"""Returns ``kappa_cmd``, nudged toward (lane-blend target + ``offset``) when active.
|
||||
|
||||
``offset`` (m): positive shifts the target right, negative left (same sign convention as
|
||||
curvature mode's ``custom_path_offset_curv``). Applied whether or not lane lines are usable
|
||||
-- see module docstring.
|
||||
``gain`` (0.0-1.0): user-tunable authority -- how much of the (already magnitude-clipped)
|
||||
raw correction is actually applied. 0 disables the trim's effect without disabling detection.
|
||||
|
||||
The applied correction is both exponentially smoothed (_SMOOTH_TAU_S) and rate-limited
|
||||
(_CORRECTION_ROC_PER_TICK) -- see the constants above -- so a lane-line-confidence
|
||||
transition (e.g. a merge lane too wide to be trusted, then narrowing back into range) eases
|
||||
into its new target instead of snapping.
|
||||
"""
|
||||
if not enabled or not lat_active or lane_change or model is None:
|
||||
self.reset()
|
||||
return kappa_cmd
|
||||
|
||||
speed_factor = float(interp(v_ego, _SPEED_RAMP_BP, _SPEED_RAMP_V))
|
||||
if speed_factor <= 0.0:
|
||||
self.reset()
|
||||
return kappa_cmd
|
||||
if not (np.isfinite(offset) and np.isfinite(gain)): #stops not a number from getting to steering command; validation of number
|
||||
self.reset()
|
||||
return kappa_cmd
|
||||
|
||||
valid, raw = self._raw_correction(model, v_ego, offset)
|
||||
if not valid:
|
||||
# Only true failure case now: model.position itself is unusable, so there's no baseline
|
||||
# to offset from at all (see _raw_correction). Lane-line-only failures fall back to the
|
||||
# model-position blend instead of landing here.
|
||||
self.reset()
|
||||
return kappa_cmd
|
||||
|
||||
target = float(np.clip(raw, -_MAX_RAW_CORRECTION, _MAX_RAW_CORRECTION)) * float(np.clip(gain, 0.0, 1.0)) * speed_factor
|
||||
alpha = 1.0 - np.exp(-0.05 / _SMOOTH_TAU_S) # BluePilot lateral tick is 20 Hz (dt=0.05s)
|
||||
filtered = float(alpha * target + (1.0 - alpha) * self._correction)
|
||||
# Rate-of-change limit -- see _CORRECTION_ROC_PER_TICK. Bounds the correction's own per-tick
|
||||
# delta on top of the exponential filter above, so a lane-line-confidence transition can't
|
||||
# snap the trim toward its new target in one or two frames.
|
||||
self._correction = float(np.clip(filtered, self._correction - _CORRECTION_ROC_PER_TICK,
|
||||
self._correction + _CORRECTION_ROC_PER_TICK))
|
||||
return kappa_cmd + self._correction
|
||||
|
||||
@property
|
||||
def correction(self) -> float:
|
||||
"""Telemetry: last applied correction (1/m)."""
|
||||
return self._correction
|
||||
|
||||
def _raw_correction(self, model, v_ego: float, offset: float) -> tuple[bool, float]:
|
||||
try:
|
||||
pos_x = np.asarray(model.position.x, dtype=float)
|
||||
pos_y = np.asarray(model.position.y, dtype=float)
|
||||
if pos_x.size < 2 or pos_x.size != pos_y.size:
|
||||
return False, 0.0
|
||||
if not (np.isfinite(pos_x).all() and np.isfinite(pos_y).all() and np.all(np.diff(pos_x) > 0)):
|
||||
return False, 0.0
|
||||
|
||||
lookahead = float(np.clip(v_ego, _LOOKAHEAD_MIN_M, _LOOKAHEAD_MAX_M))
|
||||
model_y = float(np.interp(lookahead, pos_x, pos_y))
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False, 0.0
|
||||
|
||||
# Laneline contribution, blended in by confidence. scale=0 (model-position-only, i.e. just
|
||||
# the user's offset bias) whenever lines are missing, low-confidence, or structurally bad --
|
||||
# see _laneline_blend for the confidence formula (ported from lateral_curv_ext.py).
|
||||
scale, laneline_center_y = self._laneline_blend(model, lookahead)
|
||||
target_y = model_y * (1.0 - scale) + laneline_center_y * scale
|
||||
|
||||
error = (target_y + offset) - model_y
|
||||
raw = 2.0 * error / (lookahead ** 2)
|
||||
return True, float(raw)
|
||||
|
||||
def _laneline_blend(self, model, lookahead: float) -> tuple[float, float]:
|
||||
"""Returns (scale, laneline_center_y). scale=0 whenever lanelines can't be trusted (missing,
|
||||
low-probability, structurally invalid) -- center_y is unused/meaningless in that case since
|
||||
it's weighted out by scale in the caller's blend."""
|
||||
try:
|
||||
lane_lines = model.laneLines
|
||||
probs = model.laneLineProbs
|
||||
stds = model.laneLineStds # a line could exist but this verifies how sure it is of where it is
|
||||
if len(lane_lines) < 3 or len(probs) < 3 or len(stds) < 3:
|
||||
return 0.0, 0.0
|
||||
|
||||
left_x = np.asarray(lane_lines[1].x, dtype=float)
|
||||
left_y = np.asarray(lane_lines[1].y, dtype=float)
|
||||
right_x = np.asarray(lane_lines[2].x, dtype=float)
|
||||
right_y = np.asarray(lane_lines[2].y, dtype=float)
|
||||
if (left_x.size < 2 or left_x.size != left_y.size or
|
||||
right_x.size < 2 or right_x.size != right_y.size):
|
||||
return 0.0, 0.0
|
||||
if not (np.isfinite(left_x).all() and np.isfinite(left_y).all() and
|
||||
np.isfinite(right_x).all() and np.isfinite(right_y).all()):
|
||||
return 0.0, 0.0
|
||||
if not (np.all(np.diff(left_x) > 0) and np.all(np.diff(right_x) > 0)):
|
||||
return 0.0, 0.0
|
||||
|
||||
left = float(np.interp(lookahead, left_x, left_y))
|
||||
right = float(np.interp(lookahead, right_x, right_y))
|
||||
width = right - left
|
||||
|
||||
# Same confidence formula as lateral_curv_ext.py's path_offset blend: width-tolerance
|
||||
# (penalizes implausibly wide/merging-looking detections) combined with per-line
|
||||
# probability via min() -- a single missing/unreliable line (e.g. no line on the curb
|
||||
# side, only a center stripe) drags confidence toward 0 on its own.
|
||||
width_tolerance = float(np.interp(width, _WIDTH_TOLERANCE_BP, _WIDTH_TOLERANCE_V))
|
||||
std_tolerance = float(np.interp(max(float(stds[1]), float(stds[2])), _STD_TOLERANCE_BP, _STD_TOLERANCE_V)) #StarPilot stopped at 0.3, this fades the std through the table
|
||||
confidence = min(float(probs[1]), float(probs[2]), width_tolerance, std_tolerance) #confidence is the weakest signal
|
||||
scale = float(np.clip(np.interp(confidence, _CONFIDENCE_BP, _CONFIDENCE_V), 0.0, 1.0))
|
||||
center_y = 0.5 * (left + right)
|
||||
return scale, center_y
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return 0.0, 0.0
|
||||
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
BluePilot Ford CAN message builder extensions.
|
||||
|
||||
Provides extended versions of the stock fordcan.py message builders with additional
|
||||
parameters for BluePilot features:
|
||||
- Dynamic ramp_type and precision_type for lateral control
|
||||
- Split brake_actuate / precharge_actuate for smoother deceleration
|
||||
- BlueCruise cluster HUD status, TJA warnings, and hands-free messaging
|
||||
- Independent left/right lane line logic for LKAS UI
|
||||
- ICBM button injection for intelligent cruise button management
|
||||
|
||||
Each function mirrors the stock fordcan.py function it replaces, with an extended
|
||||
parameter list. When stock carcontroller.py is refactored, it will import these
|
||||
functions instead of the stock versions.
|
||||
"""
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.fordcan import CanBus, calculate_lat_ctl2_checksum
|
||||
|
||||
HUDControl = structs.CarControl.HUDControl
|
||||
|
||||
|
||||
_BP_LKA_SHADOW_CURVATURE_SCALE = 1e-6 # 1/meter per raw unit, matches ford.h's decode
|
||||
|
||||
|
||||
def create_lka_msg(packer, CAN: CanBus, lat_active: bool, hud_control,
|
||||
angle_mode_engaged: bool = False, shadow_curvature: float = 0.0):
|
||||
"""
|
||||
Creates a CAN message for the Ford LKA Command.
|
||||
|
||||
BluePilot extension: accepts lat_active and hud_control parameters for future
|
||||
lane departure warning integration (unused so far).
|
||||
|
||||
Also carries angle_mode_engaged + shadow_curvature packed into bits with no DBC signal mapped to
|
||||
them -- confirmed unused (always 0, no cabana signal) on real F-150 dashcam routes. This message
|
||||
is one openpilot itself originates every cycle, and ford_tx_hook already reads other fields
|
||||
(LkaActvStats_D2_Req) directly out of these same bytes synchronously, in the same tx_hook call --
|
||||
no separate CAN ID, no RX round-trip needed (panda does not self-receive its own TX, confirmed
|
||||
2026-07-09 investigating a controlsMismatch caused by an earlier dedicated-message design).
|
||||
|
||||
Byte layout (bits not covered by any Lane_Assist_Data1 DBC signal):
|
||||
byte 4 bit 0: angle_mode_engaged
|
||||
byte 4 bits 1-4: reserved (future bools)
|
||||
byte 5-6: shadow_curvature (int16, scale 1e-6 1/m)
|
||||
byte 7: reserved (future value)
|
||||
Must match the decode in ford.h's FORD_Lane_Assist_Data1 tx_hook check exactly.
|
||||
|
||||
Frequency is 33Hz.
|
||||
"""
|
||||
addr, dat, bus = packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
|
||||
dat = bytearray(dat)
|
||||
|
||||
shadow_curvature_raw = int(round(shadow_curvature / _BP_LKA_SHADOW_CURVATURE_SCALE))
|
||||
shadow_curvature_raw = max(-32768, min(32767, shadow_curvature_raw)) & 0xFFFF
|
||||
|
||||
dat[4] |= 1 if angle_mode_engaged else 0
|
||||
dat[5] = (shadow_curvature_raw >> 8) & 0xFF
|
||||
dat[6] = shadow_curvature_raw & 0xFF
|
||||
|
||||
return addr, bytes(dat), bus
|
||||
|
||||
|
||||
def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, ramp_type: int, precision_type: int,
|
||||
path_offset: float, path_angle: float, curvature: float, curvature_rate: float):
|
||||
"""
|
||||
Creates a CAN message for the Ford TJA/LCA Command (non-CAN FD).
|
||||
|
||||
BluePilot extension: dynamic ramp_type and precision_type parameters.
|
||||
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
|
||||
|
||||
Ford lane centering uses a third-order polynomial to describe the road centerline:
|
||||
c0 (path_offset): lateral offset between vehicle and centerline (positive is right)
|
||||
c1 (path_angle): heading angle between vehicle and centerline (positive is right)
|
||||
c2 (curvature): curvature of the centerline (positive is left)
|
||||
c3 (curvature_rate): rate of change of curvature
|
||||
|
||||
Frequency is 20Hz.
|
||||
"""
|
||||
values = {
|
||||
"LatCtlRng_L_Max": 0, # Unknown [0|126] meter
|
||||
"HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1]
|
||||
"LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft,
|
||||
# 3=InterventionRight, 4-7=NotUsed [0|7]
|
||||
"LatCtlRampType_D_Rq": ramp_type, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
|
||||
"LatCtlPrecision_D_Rq": precision_type, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
|
||||
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
|
||||
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
|
||||
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
|
||||
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
|
||||
}
|
||||
return packer.make_can_msg("LateralMotionControl", CAN.main, values)
|
||||
|
||||
|
||||
def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, ramp_type: int, precision_type: int,
|
||||
path_offset: float, path_angle: float, curvature: float,
|
||||
curvature_rate: float, counter: int):
|
||||
"""
|
||||
Creates a CAN message for the Ford Lane Centering command (CAN FD).
|
||||
|
||||
BluePilot extension: dynamic ramp_type and precision_type parameters.
|
||||
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
|
||||
|
||||
This message replaces LateralMotionControl on CAN FD platforms and includes
|
||||
counter and checksum fields.
|
||||
|
||||
Frequency is 20Hz.
|
||||
"""
|
||||
values = {
|
||||
"LatCtl_D2_Rq": mode, # Mode: 0=None, 1=PathFollowingLimitedMode, 2=PathFollowingExtendedMode,
|
||||
# 3=SafeRampOut, 4-7=NotUsed [0|7]
|
||||
"LatCtlRampType_D_Rq": ramp_type, # 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
|
||||
"LatCtlPrecision_D_Rq": precision_type, # 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
|
||||
"LatCtlPathOffst_L_Actl": path_offset, # [-5.12|5.11] meter
|
||||
"LatCtlPath_An_Actl": path_angle, # [-0.5|0.5235] radians
|
||||
"LatCtlCurv_No_Actl": curvature, # [-0.02|0.02094] 1/meter
|
||||
"LatCtlCrv_NoRate2_Actl": curvature_rate, # [-0.001024|0.001023] 1/meter^2
|
||||
"HandsOffCnfm_B_Rq": 0, # 0=Inactive, 1=Active [0|1]
|
||||
"LatCtlPath_No_Cnt": counter, # [0|15]
|
||||
"LatCtlPath_No_Cs": 0, # [0|255]
|
||||
}
|
||||
|
||||
# Calculate checksum (reuse stock function)
|
||||
dat = packer.make_can_msg("LateralMotionControl2", 0, values)[1]
|
||||
values["LatCtlPath_No_Cs"] = calculate_lat_ctl2_checksum(mode, counter, dat)
|
||||
|
||||
return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
|
||||
|
||||
|
||||
def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: float, accel_pred: float,
|
||||
stopping: bool, brake_actuate: bool, precharge_actuate: bool, v_ego_kph: float):
|
||||
"""
|
||||
Creates a CAN message for the Ford ACC Command.
|
||||
|
||||
BluePilot extension: split brake control into brake_actuate and precharge_actuate
|
||||
(each with independent hysteresis thresholds) for smoother deceleration. Also
|
||||
accepts accel_pred as a parameter instead of hardcoding -5.0.
|
||||
|
||||
Precharge engages slightly before full brake for smoother initial decel feel.
|
||||
Both use configurable hysteresis to avoid binary on/off feel.
|
||||
|
||||
Frequency is 50Hz.
|
||||
"""
|
||||
values = {
|
||||
"AccBrkTot_A_Rq": accel, # Brake total accel request: [-20|11.9449] m/s^2
|
||||
"Cmbb_B_Enbl": 1 if long_active else 0, # Enabled: 0=No, 1=Yes
|
||||
"AccPrpl_A_Rq": gas, # Acceleration request: [-5|5.23] m/s^2
|
||||
"AccPrpl_A_Pred": accel_pred, # Predicted accel (from carcontroller, not hardcoded)
|
||||
"AccResumEnbl_B_Rq": 1 if long_active else 0,
|
||||
"AccVeh_V_Trg": v_ego_kph, # Target speed: [0|255] km/h
|
||||
"AccBrkPrchg_B_Rq": 1 if precharge_actuate else 0, # Pre-charge brake request (independent hysteresis)
|
||||
"AccBrkDecel_B_Rq": 1 if brake_actuate else 0, # Deceleration request (independent hysteresis)
|
||||
"AccStopStat_B_Rq": 1 if stopping else 0,
|
||||
}
|
||||
return packer.make_can_msg("ACCDATA", CAN.main, values)
|
||||
|
||||
|
||||
def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw_alert: bool,
|
||||
standstill: bool, hud_control, stock_values: dict, send_hands_free_msg: bool,
|
||||
send_ui: bool, send_bars: bool, tja_warn: int, tja_msg: int):
|
||||
"""
|
||||
Creates a CAN message for the Ford IPC adaptive cruise, FCW and TJA status.
|
||||
|
||||
BluePilot extension: replaces stock show_distance_bars with explicit send_ui,
|
||||
send_bars, and TJA parameters. Adds BlueCruise status 7 for hands-free cluster
|
||||
UI. TJA warn/msg are set from DM state computation rather than stock passthrough.
|
||||
|
||||
Stock functionality is maintained by passing through unmodified signals.
|
||||
|
||||
Frequency is 5Hz.
|
||||
"""
|
||||
|
||||
# Tja_D_Stat: TJA status for cluster display
|
||||
if enabled:
|
||||
if hud_control.leftLaneDepart:
|
||||
status = 3 # ActiveInterventionLeft
|
||||
elif hud_control.rightLaneDepart:
|
||||
status = 4 # ActiveInterventionRight
|
||||
elif send_hands_free_msg:
|
||||
status = 7 # BlueCruise UI in the cluster
|
||||
else:
|
||||
status = 2 # Active
|
||||
elif main_on:
|
||||
if hud_control.leftLaneDepart:
|
||||
status = 5 # ActiveWarningLeft
|
||||
elif hud_control.rightLaneDepart:
|
||||
status = 6 # ActiveWarningRight
|
||||
else:
|
||||
status = 1 # Standby
|
||||
elif standstill:
|
||||
status = 0 # Off
|
||||
else:
|
||||
status = 1 # Standby
|
||||
|
||||
values = {s: stock_values[s] for s in [
|
||||
"HaDsply_No_Cs",
|
||||
"HaDsply_No_Cnt",
|
||||
"AccStopStat_D_Dsply", # ACC stopped status message
|
||||
"AccTrgDist2_D_Dsply", # ACC target distance
|
||||
"AccStopRes_B_Dsply",
|
||||
# TjaWarn_D_Rq and TjaMsgTxt_D_Dsply are set explicitly below, not passed through
|
||||
"IaccLamp_D_Rq", # iACC status icon
|
||||
"AccMsgTxt_D2_Rq", # ACC text
|
||||
"FcwDeny_B_Dsply", # FCW disabled
|
||||
"FcwMemStat_B_Actl", # FCW enabled setting
|
||||
"AccTGap_B_Dsply", # ACC time gap display setting
|
||||
"CadsAlignIncplt_B_Actl",
|
||||
"AccFllwMde_B_Dsply", # ACC follow mode display setting
|
||||
"CadsRadrBlck_B_Actl",
|
||||
"CmbbPostEvnt_B_Dsply", # AEB event status
|
||||
"AccStopMde_B_Dsply", # ACC stop mode display setting
|
||||
"FcwMemSens_D_Actl", # FCW sensitivity setting
|
||||
"FcwMsgTxt_D_Rq", # FCW text
|
||||
"AccWarn_D_Dsply", # ACC warning
|
||||
"FcwVisblWarn_B_Rq", # FCW visible alert
|
||||
"FcwAudioWarn_B_Rq", # FCW audio alert
|
||||
"AccTGap_D_Dsply", # ACC time gap
|
||||
"AccMemEnbl_B_RqDrv", # ACC adaptive/normal setting
|
||||
"FdaMem_B_Stat", # FDA enabled setting
|
||||
]}
|
||||
|
||||
values.update({
|
||||
"Tja_D_Stat": status, # TJA status
|
||||
"TjaWarn_D_Rq": tja_warn, # TJA warning (from DM state, not stock passthrough)
|
||||
"TjaMsgTxt_D_Dsply": tja_msg, # TJA text (from DM state, not stock passthrough)
|
||||
})
|
||||
|
||||
if CP.openpilotLongitudinalControl:
|
||||
values.update({
|
||||
"AccStopStat_D_Dsply": 2 if standstill else 0, # Stopping status text
|
||||
"AccMsgTxt_D2_Rq": 0, # ACC text
|
||||
"AccTGap_B_Dsply": 1 if send_bars else 0, # Show time gap control UI
|
||||
"AccFllwMde_B_Dsply": 1 if hud_control.leadVisible else 0, # Lead indicator
|
||||
"AccStopMde_B_Dsply": 1 if standstill else 0,
|
||||
"AccWarn_D_Dsply": 0, # ACC warning
|
||||
"AccTGap_D_Dsply": hud_control.leadDistanceBars, # Time gap
|
||||
})
|
||||
|
||||
# Forward FCW alert from IPMA
|
||||
if fcw_alert:
|
||||
values["FcwVisblWarn_B_Rq"] = 1 # FCW visible alert
|
||||
values["FcwAudioWarn_B_Rq"] = 1 # FCW audio alert
|
||||
|
||||
return packer.make_can_msg("ACCDATA_3", CAN.main, values)
|
||||
|
||||
|
||||
def create_lkas_ui_msg(packer, CAN: CanBus, main_on: bool, enabled: bool, hands: int,
|
||||
hud_control, stock_values: dict):
|
||||
"""
|
||||
Creates a CAN message for the Ford IPC IPMA/LKAS status.
|
||||
|
||||
BluePilot extension: replaces stock steer_alert bool with hands int (0-3),
|
||||
and uses independent left/right lane line status logic.
|
||||
|
||||
hands values:
|
||||
0 = HandsOn
|
||||
1 = Level1 (warning without chime)
|
||||
2 = Level2 (warning with chime)
|
||||
3 = Suppressed
|
||||
|
||||
LaActvStats_D_Dsply value table (left \\ right):
|
||||
Right → | Intervene | Warning | Suppress | Available | None
|
||||
Intervene | 24 | 19 | 14 | 9 | 4
|
||||
Warning | 23 | 18 | 13 | 8 | 3
|
||||
Suppress | 22 | 17 | 12 | 7 | 2
|
||||
Available | 21 | 16 | 11 | 6 | 1
|
||||
None | 20 | 15 | 10 | 5 | 0
|
||||
|
||||
Stock functionality is maintained by passing through unmodified signals.
|
||||
|
||||
Frequency is 1Hz.
|
||||
"""
|
||||
|
||||
lines = 0
|
||||
|
||||
if hud_control is not None:
|
||||
# Determine left lane status independently
|
||||
if hud_control.leftLaneDepart:
|
||||
left_status = 4 # Intervene (Yellow)
|
||||
elif hud_control.leftLaneVisible:
|
||||
left_status = 1 # Available
|
||||
else:
|
||||
left_status = 2 # Suppress
|
||||
|
||||
# Determine right lane status independently
|
||||
if hud_control.rightLaneDepart:
|
||||
right_status = 20 # Intervene (Yellow)
|
||||
elif hud_control.rightLaneVisible:
|
||||
right_status = 5 # Available
|
||||
else:
|
||||
right_status = 10 # Suppress
|
||||
|
||||
# Combine left and right lane status
|
||||
lines = left_status + right_status
|
||||
|
||||
values = {s: stock_values[s] for s in [
|
||||
"FeatConfigIpmaActl",
|
||||
"FeatNoIpmaActl",
|
||||
"PersIndexIpma_D_Actl",
|
||||
"AhbcRampingV_D_Rq", # AHB ramping
|
||||
"LaDenyStats_B_Dsply", # LKAS error
|
||||
"CamraDefog_B_Req", # Windshield heater?
|
||||
"CamraStats_D_Dsply", # Camera status
|
||||
"DasAlrtLvl_D_Dsply", # DAS alert level
|
||||
"DasStats_D_Dsply", # DAS status
|
||||
"DasWarn_D_Dsply", # DAS warning
|
||||
"AhbHiBeam_D_Rq", # AHB status
|
||||
"Passthru_63",
|
||||
"Passthru_48",
|
||||
]}
|
||||
|
||||
values.update({
|
||||
"LaActvStats_D_Dsply": lines, # LKAS status (lane lines) [0|31]
|
||||
"LaHandsOff_D_Dsply": hands, # 0=HandsOn, 1=Level1 (w/o chime), 2=Level2 (w/ chime), 3=Suppressed
|
||||
})
|
||||
return packer.make_can_msg("IPMA_Data", CAN.main, values)
|
||||
|
||||
|
||||
def create_button_msg(packer, bus: int, stock_values: dict, cancel=False, resume=False,
|
||||
tja_toggle=False, icbm_button=None):
|
||||
"""
|
||||
Creates a CAN message for the Ford SCCM buttons/switches.
|
||||
|
||||
BluePilot extension: adds icbm_button parameter for Intelligent Cruise Button
|
||||
Management. When set, the specified CAN signal is set to 1 in the outgoing
|
||||
message, enabling openpilot to emulate cruise button presses for speed adjustment.
|
||||
|
||||
Args:
|
||||
icbm_button: Optional string signal name (e.g., "CcAslButtnSetIncPress",
|
||||
"CcAslButtnSetDecPress") for ICBM button injection.
|
||||
|
||||
Frequency is 10Hz.
|
||||
"""
|
||||
values = {s: stock_values[s] for s in [
|
||||
"HeadLghtHiFlash_D_Stat", # SCCM Passthrough the remaining buttons
|
||||
"TurnLghtSwtch_D_Stat", # SCCM Turn signal switch
|
||||
"WiprFront_D_Stat",
|
||||
"LghtAmb_D_Sns",
|
||||
"AccButtnGapDecPress",
|
||||
"AccButtnGapIncPress",
|
||||
"AslButtnOnOffCnclPress",
|
||||
"AslButtnOnOffPress",
|
||||
"LaSwtchPos_D_Stat",
|
||||
"CcAslButtnCnclResPress",
|
||||
"CcAslButtnDeny_B_Actl",
|
||||
"CcAslButtnIndxDecPress",
|
||||
"CcAslButtnIndxIncPress",
|
||||
"CcAslButtnOffCnclPress",
|
||||
"CcAslButtnOnOffCncl",
|
||||
"CcAslButtnOnPress",
|
||||
"CcAslButtnResDecPress",
|
||||
"CcAslButtnResIncPress",
|
||||
"CcAslButtnSetDecPress",
|
||||
"CcAslButtnSetIncPress",
|
||||
"CcAslButtnSetPress",
|
||||
"CcButtnOffPress",
|
||||
"CcButtnOnOffCnclPress",
|
||||
"CcButtnOnOffPress",
|
||||
"CcButtnOnPress",
|
||||
"HeadLghtHiFlash_D_Actl",
|
||||
"HeadLghtHiOn_B_StatAhb",
|
||||
"AhbStat_B_Dsply",
|
||||
"AccButtnGapTogglePress",
|
||||
"WiprFrontSwtch_D_Stat",
|
||||
"HeadLghtHiCtrl_D_RqAhb",
|
||||
]}
|
||||
|
||||
values.update({
|
||||
"CcAslButtnCnclPress": 1 if cancel else 0, # CC cancel button
|
||||
"CcAsllButtnResPress": 1 if resume else 0, # CC resume button
|
||||
"TjaButtnOnOffPress": 1 if tja_toggle else 0, # LCA/TJA toggle button
|
||||
})
|
||||
|
||||
# ICBM button support — set the specified button signal to 1
|
||||
if icbm_button is not None:
|
||||
values[icbm_button] = 1
|
||||
|
||||
return packer.make_can_msg("Steering_Data_FD1", bus, values)
|
||||
@@ -0,0 +1,342 @@
|
||||
from opendbc.car import CanBusBase, structs
|
||||
|
||||
HUDControl = structs.CarControl.HUDControl
|
||||
|
||||
|
||||
class CanBus(CanBusBase):
|
||||
def __init__(self, CP=None, fingerprint=None) -> None:
|
||||
super().__init__(CP, fingerprint)
|
||||
|
||||
@property
|
||||
def main(self) -> int:
|
||||
return self.offset
|
||||
|
||||
@property
|
||||
def radar(self) -> int:
|
||||
return self.offset + 1
|
||||
|
||||
@property
|
||||
def camera(self) -> int:
|
||||
return self.offset + 2
|
||||
|
||||
|
||||
def calculate_lat_ctl2_checksum(mode: int, counter: int, dat: bytearray) -> int:
|
||||
curvature = (dat[2] << 3) | ((dat[3]) >> 5)
|
||||
curvature_rate = (dat[6] << 3) | ((dat[7]) >> 5)
|
||||
path_angle = ((dat[3] & 0x1F) << 6) | ((dat[4]) >> 2)
|
||||
path_offset = ((dat[4] & 0x3) << 8) | dat[5]
|
||||
|
||||
checksum = mode + counter
|
||||
for sig_val in (curvature, curvature_rate, path_angle, path_offset):
|
||||
checksum += sig_val + (sig_val >> 8)
|
||||
|
||||
return 0xFF - (checksum & 0xFF)
|
||||
|
||||
|
||||
def create_lka_msg(packer, CAN: CanBus):
|
||||
"""
|
||||
Creates an empty CAN message for the Ford LKA Command.
|
||||
|
||||
This command can apply "Lane Keeping Aid" maneuvers, which are subject to the PSCM lockout.
|
||||
|
||||
Frequency is 33Hz.
|
||||
"""
|
||||
|
||||
return packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
|
||||
|
||||
|
||||
def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, path_offset: float, path_angle: float, curvature: float,
|
||||
curvature_rate: float):
|
||||
"""
|
||||
Creates a CAN message for the Ford TJA/LCA Command.
|
||||
|
||||
This command can apply "Lane Centering" maneuvers: continuous lane centering for traffic jam assist and highway
|
||||
driving. It is not subject to the PSCM lockout.
|
||||
|
||||
Ford lane centering command uses a third order polynomial to describe the road centerline. The polynomial is defined
|
||||
by the following coefficients:
|
||||
c0: lateral offset between the vehicle and the centerline (positive is right)
|
||||
c1: heading angle between the vehicle and the centerline (positive is right)
|
||||
c2: curvature of the centerline (positive is left)
|
||||
c3: rate of change of curvature of the centerline
|
||||
As the PSCM combines this information with other sensor data, such as the vehicle's yaw rate and speed, the steering
|
||||
angle cannot be easily controlled.
|
||||
|
||||
The PSCM should be configured to accept TJA/LCA commands before these commands will be processed. This can be done
|
||||
using tools such as Forscan.
|
||||
|
||||
Frequency is 20Hz.
|
||||
"""
|
||||
|
||||
values = {
|
||||
"LatCtlRng_L_Max": 0, # Unknown [0|126] meter
|
||||
"HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1]
|
||||
"LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft,
|
||||
# 3=InterventionRight, 4-7=NotUsed [0|7]
|
||||
"LatCtlRampType_D_Rq": 0, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
|
||||
# Makes no difference with curvature control
|
||||
"LatCtlPrecision_D_Rq": 1, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
|
||||
# The stock system always uses comfortable
|
||||
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
|
||||
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
|
||||
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
|
||||
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
|
||||
}
|
||||
return packer.make_can_msg("LateralMotionControl", CAN.main, values)
|
||||
|
||||
|
||||
def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, path_offset: float, path_angle: float, curvature: float,
|
||||
curvature_rate: float, counter: int):
|
||||
"""
|
||||
Create a CAN message for the new Ford Lane Centering command.
|
||||
|
||||
This message is used on the CAN FD platform and replaces the old LateralMotionControl message. It is similar but has
|
||||
additional signals for a counter and checksum.
|
||||
|
||||
Frequency is 20Hz.
|
||||
"""
|
||||
|
||||
values = {
|
||||
"LatCtl_D2_Rq": mode, # Mode: 0=None, 1=PathFollowingLimitedMode, 2=PathFollowingExtendedMode,
|
||||
# 3=SafeRampOut, 4-7=NotUsed [0|7]
|
||||
"LatCtlRampType_D_Rq": 0, # 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
|
||||
"LatCtlPrecision_D_Rq": 1, # 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
|
||||
"LatCtlPathOffst_L_Actl": path_offset, # [-5.12|5.11] meter
|
||||
"LatCtlPath_An_Actl": path_angle, # [-0.5|0.5235] radians
|
||||
"LatCtlCurv_No_Actl": curvature, # [-0.02|0.02094] 1/meter
|
||||
"LatCtlCrv_NoRate2_Actl": curvature_rate, # [-0.001024|0.001023] 1/meter^2
|
||||
"HandsOffCnfm_B_Rq": 0, # 0=Inactive, 1=Active [0|1]
|
||||
"LatCtlPath_No_Cnt": counter, # [0|15]
|
||||
"LatCtlPath_No_Cs": 0, # [0|255]
|
||||
}
|
||||
|
||||
# calculate checksum
|
||||
dat = packer.make_can_msg("LateralMotionControl2", 0, values)[1]
|
||||
values["LatCtlPath_No_Cs"] = calculate_lat_ctl2_checksum(mode, counter, dat)
|
||||
|
||||
return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
|
||||
|
||||
|
||||
def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: float, stopping: bool, brake_request, v_ego_kph: float):
|
||||
"""
|
||||
Creates a CAN message for the Ford ACC Command.
|
||||
|
||||
This command can be used to enable ACC, to set the ACC gas/brake/decel values
|
||||
and to disable ACC.
|
||||
|
||||
Frequency is 50Hz.
|
||||
"""
|
||||
values = {
|
||||
"AccBrkTot_A_Rq": accel, # Brake total accel request: [-20|11.9449] m/s^2
|
||||
"Cmbb_B_Enbl": 1 if long_active else 0, # Enabled: 0=No, 1=Yes
|
||||
"AccPrpl_A_Rq": gas, # Acceleration request: [-5|5.23] m/s^2
|
||||
# No observed acceleration seen from this signal alone. During stock system operation, it appears to
|
||||
# be the raw acceleration request (AccPrpl_A_Rq when positive, AccBrkTot_A_Rq when negative)
|
||||
"AccPrpl_A_Pred": -5.0, # Acceleration request: [-5|5.23] m/s^2
|
||||
"AccResumEnbl_B_Rq": 1 if long_active else 0,
|
||||
# No observed acceleration seen from this signal alone
|
||||
"AccVeh_V_Trg": v_ego_kph, # Target speed: [0|255] km/h
|
||||
# TODO: we may be able to improve braking response by utilizing pre-charging better
|
||||
# When setting these two bits without AccBrkTot_A_Rq, an initial jerk is observed and car may be able to brake temporarily with AccPrpl_A_Rq
|
||||
"AccBrkPrchg_B_Rq": 1 if brake_request else 0, # Pre-charge brake request: 0=No, 1=Yes
|
||||
"AccBrkDecel_B_Rq": 1 if brake_request else 0, # Deceleration request: 0=Inactive, 1=Active
|
||||
"AccStopStat_B_Rq": 1 if stopping else 0,
|
||||
}
|
||||
return packer.make_can_msg("ACCDATA", CAN.main, values)
|
||||
|
||||
|
||||
def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw_alert: bool, standstill: bool,
|
||||
show_distance_bars: bool, hud_control, stock_values: dict):
|
||||
"""
|
||||
Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam
|
||||
assist status.
|
||||
|
||||
Stock functionality is maintained by passing through unmodified signals.
|
||||
|
||||
Frequency is 5Hz.
|
||||
"""
|
||||
|
||||
# Tja_D_Stat
|
||||
if enabled:
|
||||
if hud_control.leftLaneDepart:
|
||||
status = 3 # ActiveInterventionLeft
|
||||
elif hud_control.rightLaneDepart:
|
||||
status = 4 # ActiveInterventionRight
|
||||
else:
|
||||
status = 2 # Active
|
||||
elif main_on:
|
||||
if hud_control.leftLaneDepart:
|
||||
status = 5 # ActiveWarningLeft
|
||||
elif hud_control.rightLaneDepart:
|
||||
status = 6 # ActiveWarningRight
|
||||
else:
|
||||
status = 1 # Standby
|
||||
else:
|
||||
status = 0 # Off
|
||||
|
||||
values = {s: stock_values[s] for s in [
|
||||
"HaDsply_No_Cs",
|
||||
"HaDsply_No_Cnt",
|
||||
"AccStopStat_D_Dsply", # ACC stopped status message
|
||||
"AccTrgDist2_D_Dsply", # ACC target distance
|
||||
"AccStopRes_B_Dsply",
|
||||
"TjaWarn_D_Rq", # TJA warning
|
||||
"TjaMsgTxt_D_Dsply", # TJA text
|
||||
"IaccLamp_D_Rq", # iACC status icon
|
||||
"AccMsgTxt_D2_Rq", # ACC text
|
||||
"FcwDeny_B_Dsply", # FCW disabled
|
||||
"FcwMemStat_B_Actl", # FCW enabled setting
|
||||
"AccTGap_B_Dsply", # ACC time gap display setting
|
||||
"CadsAlignIncplt_B_Actl",
|
||||
"AccFllwMde_B_Dsply", # ACC follow mode display setting
|
||||
"CadsRadrBlck_B_Actl",
|
||||
"CmbbPostEvnt_B_Dsply", # AEB event status
|
||||
"AccStopMde_B_Dsply", # ACC stop mode display setting
|
||||
"FcwMemSens_D_Actl", # FCW sensitivity setting
|
||||
"FcwMsgTxt_D_Rq", # FCW text
|
||||
"AccWarn_D_Dsply", # ACC warning
|
||||
"FcwVisblWarn_B_Rq", # FCW visible alert
|
||||
"FcwAudioWarn_B_Rq", # FCW audio alert
|
||||
"AccTGap_D_Dsply", # ACC time gap
|
||||
"AccMemEnbl_B_RqDrv", # ACC adaptive/normal setting
|
||||
"FdaMem_B_Stat", # FDA enabled setting
|
||||
]}
|
||||
|
||||
values.update({
|
||||
"Tja_D_Stat": status, # TJA status
|
||||
})
|
||||
|
||||
if CP.openpilotLongitudinalControl:
|
||||
values.update({
|
||||
"AccStopStat_D_Dsply": 2 if standstill else 0, # Stopping status text
|
||||
"AccMsgTxt_D2_Rq": 0, # ACC text
|
||||
"AccTGap_B_Dsply": 1 if show_distance_bars else 0, # Show time gap control UI
|
||||
"AccFllwMde_B_Dsply": 1 if hud_control.leadVisible else 0, # Lead indicator
|
||||
"AccStopMde_B_Dsply": 1 if standstill else 0,
|
||||
"AccWarn_D_Dsply": 0, # ACC warning
|
||||
"AccTGap_D_Dsply": hud_control.leadDistanceBars, # Time gap
|
||||
})
|
||||
|
||||
# Forwards FCW alert from IPMA
|
||||
if fcw_alert:
|
||||
values["FcwVisblWarn_B_Rq"] = 1 # FCW visible alert
|
||||
|
||||
return packer.make_can_msg("ACCDATA_3", CAN.main, values)
|
||||
|
||||
|
||||
def create_lkas_ui_msg(packer, CAN: CanBus, main_on: bool, enabled: bool, steer_alert: bool, hud_control,
|
||||
stock_values: dict):
|
||||
"""
|
||||
Creates a CAN message for the Ford IPC IPMA/LKAS status.
|
||||
|
||||
Show the LKAS status with the "driver assist" lines in the IPC.
|
||||
|
||||
Stock functionality is maintained by passing through unmodified signals.
|
||||
|
||||
Frequency is 1Hz.
|
||||
"""
|
||||
|
||||
# LaActvStats_D_Dsply
|
||||
# R Intvn Warn Supprs Avail No
|
||||
# L
|
||||
# Intvn 24 19 14 9 4
|
||||
# Warn 23 18 13 8 3
|
||||
# Supprs 22 17 12 7 2
|
||||
# Avail 21 16 11 6 1
|
||||
# No 20 15 10 5 0
|
||||
#
|
||||
# TODO: test suppress state
|
||||
if enabled:
|
||||
lines = 0 # NoLeft_NoRight
|
||||
if hud_control.leftLaneDepart:
|
||||
lines += 4
|
||||
elif hud_control.leftLaneVisible:
|
||||
lines += 1
|
||||
if hud_control.rightLaneDepart:
|
||||
lines += 20
|
||||
elif hud_control.rightLaneVisible:
|
||||
lines += 5
|
||||
elif main_on:
|
||||
lines = 0
|
||||
else:
|
||||
if hud_control.leftLaneDepart:
|
||||
lines = 3 # WarnLeft_NoRight
|
||||
elif hud_control.rightLaneDepart:
|
||||
lines = 15 # NoLeft_WarnRight
|
||||
else:
|
||||
lines = 30 # LA_Off
|
||||
|
||||
hands_on_wheel_dsply = 1 if steer_alert else 0
|
||||
|
||||
values = {s: stock_values[s] for s in [
|
||||
"FeatConfigIpmaActl",
|
||||
"FeatNoIpmaActl",
|
||||
"PersIndexIpma_D_Actl",
|
||||
"AhbcRampingV_D_Rq", # AHB ramping
|
||||
"LaDenyStats_B_Dsply", # LKAS error
|
||||
"CamraDefog_B_Req", # Windshield heater?
|
||||
"CamraStats_D_Dsply", # Camera status
|
||||
"DasAlrtLvl_D_Dsply", # DAS alert level
|
||||
"DasStats_D_Dsply", # DAS status
|
||||
"DasWarn_D_Dsply", # DAS warning
|
||||
"AhbHiBeam_D_Rq", # AHB status
|
||||
"Passthru_63",
|
||||
"Passthru_48",
|
||||
]}
|
||||
|
||||
values.update({
|
||||
"LaActvStats_D_Dsply": lines, # LKAS status (lines) [0|31]
|
||||
"LaHandsOff_D_Dsply": hands_on_wheel_dsply, # 0=HandsOn, 1=Level1 (w/o chime), 2=Level2 (w/ chime), 3=Suppressed
|
||||
})
|
||||
return packer.make_can_msg("IPMA_Data", CAN.main, values)
|
||||
|
||||
|
||||
def create_button_msg(packer, bus: int, stock_values: dict, cancel=False, resume=False, tja_toggle=False):
|
||||
"""
|
||||
Creates a CAN message for the Ford SCCM buttons/switches.
|
||||
|
||||
Includes cruise control buttons, turn lights and more.
|
||||
|
||||
Frequency is 10Hz.
|
||||
"""
|
||||
|
||||
values = {s: stock_values[s] for s in [
|
||||
"HeadLghtHiFlash_D_Stat", # SCCM Passthrough the remaining buttons
|
||||
"TurnLghtSwtch_D_Stat", # SCCM Turn signal switch
|
||||
"WiprFront_D_Stat",
|
||||
"LghtAmb_D_Sns",
|
||||
"AccButtnGapDecPress",
|
||||
"AccButtnGapIncPress",
|
||||
"AslButtnOnOffCnclPress",
|
||||
"AslButtnOnOffPress",
|
||||
"LaSwtchPos_D_Stat",
|
||||
"CcAslButtnCnclResPress",
|
||||
"CcAslButtnDeny_B_Actl",
|
||||
"CcAslButtnIndxDecPress",
|
||||
"CcAslButtnIndxIncPress",
|
||||
"CcAslButtnOffCnclPress",
|
||||
"CcAslButtnOnOffCncl",
|
||||
"CcAslButtnOnPress",
|
||||
"CcAslButtnResDecPress",
|
||||
"CcAslButtnResIncPress",
|
||||
"CcAslButtnSetDecPress",
|
||||
"CcAslButtnSetIncPress",
|
||||
"CcAslButtnSetPress",
|
||||
"CcButtnOffPress",
|
||||
"CcButtnOnOffCnclPress",
|
||||
"CcButtnOnOffPress",
|
||||
"CcButtnOnPress",
|
||||
"HeadLghtHiFlash_D_Actl",
|
||||
"HeadLghtHiOn_B_StatAhb",
|
||||
"AhbStat_B_Dsply",
|
||||
"AccButtnGapTogglePress",
|
||||
"WiprFrontSwtch_D_Stat",
|
||||
"HeadLghtHiCtrl_D_RqAhb",
|
||||
]}
|
||||
|
||||
values.update({
|
||||
"CcAslButtnCnclPress": 1 if cancel else 0, # CC cancel button
|
||||
"CcAsllButtnResPress": 1 if resume else 0, # CC resume button
|
||||
"TjaButtnOnOffPress": 1 if tja_toggle else 0, # LCA/TJA toggle button
|
||||
})
|
||||
return packer.make_can_msg("Steering_Data_FD1", bus, values)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
BluePilot: shared manual-steering-override ("human turn") detection for Ford lateral control.
|
||||
|
||||
Both Ford lateral strategies hand control back to the driver on a sustained manual turn, and the
|
||||
detection is identical, so it lives here as one class instead of being copied into each mode. What
|
||||
each mode DOES with the signal differs: curvature-primary (``lateral_curv_ext``) zeroes its command
|
||||
(reset_steering + post-reset ramp); angle-primary (``lateral_angle_ext``) forces lateral inactive
|
||||
(mode 0 on the wire) and ramps path_angle back in from zero through its soft ROC on release.
|
||||
"""
|
||||
from opendbc.car import DT_CTRL
|
||||
from opendbc.car.ford.values import CarControllerParams
|
||||
|
||||
# Require sustained hands-on AND a large angle (avoids resetting on small wheel nudges in a curve).
|
||||
HUMAN_TURN_ANGLE_DEG = 45.0
|
||||
HUMAN_TURN_HOLD_S = 1.5
|
||||
# When the wheel was ALREADY past HUMAN_TURN_ANGLE_DEG at first contact -- lateral control had it
|
||||
# turned mid-curve -- the angle condition is pre-satisfied, so a brief corrective nudge would latch
|
||||
# after only HUMAN_TURN_HOLD_S of light contact and kill steering mid-curve. Require a longer hold
|
||||
# there before reading it as an intentional takeover. Route 000000bd (2026-07-14) showed the
|
||||
# discriminator holds on-road: all 7 deliberate turns began with the wheel below the threshold
|
||||
# (driver wound it up through 45 deg); only mid-maneuver grabs/nudges began beyond it.
|
||||
HUMAN_TURN_HOLD_PRETURNED_S = 3.0
|
||||
_STEER_DT = CarControllerParams.STEER_STEP * DT_CTRL # 20 Hz lateral tick
|
||||
|
||||
|
||||
class HumanTurnDetector:
|
||||
"""Latches ``active`` once the driver holds real steering pressure AND ``|wheel angle|`` >
|
||||
``HUMAN_TURN_ANGLE_DEG`` continuously for ``HUMAN_TURN_HOLD_S`` — long enough to tell an
|
||||
intentional turn from a brief nudge. ``just_released`` pulses True on the first frame after the
|
||||
override clears, so a mode can re-seed its command as it re-engages.
|
||||
|
||||
Call ``update`` once per lateral tick while control is active. Modes that want the timer to zero
|
||||
on disengage call ``reset`` on their inactive path; modes that want it to persist simply stop
|
||||
calling ``update`` (the timer holds its value).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.hold_timer_s = 0.0
|
||||
self.active = False
|
||||
self._active_last = False
|
||||
self._pressed_last = False
|
||||
self._press_started_preturned = False
|
||||
|
||||
def update(self, enabled: bool, steering_pressed: bool, steering_angle_deg: float) -> bool:
|
||||
self._active_last = self.active
|
||||
# Was the wheel already past the angle threshold when this press began? If so the driver is
|
||||
# touching a wheel that lateral control turned (mid-curve nudge), not driving a turn -- hold
|
||||
# the longer HUMAN_TURN_HOLD_PRETURNED_S before latching.
|
||||
if steering_pressed and not self._pressed_last:
|
||||
self._press_started_preturned = abs(steering_angle_deg) > HUMAN_TURN_ANGLE_DEG
|
||||
self._pressed_last = steering_pressed
|
||||
if not enabled:
|
||||
self.hold_timer_s = 0.0
|
||||
elif steering_pressed and abs(steering_angle_deg) > HUMAN_TURN_ANGLE_DEG:
|
||||
self.hold_timer_s += _STEER_DT
|
||||
else:
|
||||
self.hold_timer_s = 0.0
|
||||
hold_req = HUMAN_TURN_HOLD_PRETURNED_S if self._press_started_preturned else HUMAN_TURN_HOLD_S
|
||||
self.active = self.hold_timer_s >= hold_req
|
||||
return self.active
|
||||
|
||||
@property
|
||||
def just_released(self) -> bool:
|
||||
return self._active_last and not self.active
|
||||
|
||||
def reset(self) -> None:
|
||||
self.hold_timer_s = 0.0
|
||||
self.active = False
|
||||
self._active_last = False
|
||||
self._pressed_last = False
|
||||
self._press_started_preturned = False
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"angle": {
|
||||
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/lateral_angle_ext.py",
|
||||
"sha256": "18fa5290edade52137c7c6705a2eb2d42c94d39d1fa649eae06b7828c68207b3",
|
||||
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
|
||||
},
|
||||
"human": {
|
||||
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/human_turn.py",
|
||||
"sha256": "82735e348c5ce3e7b2c6a28ea3e298cca71ae8867433c270f7b54a9ee315ef37",
|
||||
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
|
||||
},
|
||||
"builders": {
|
||||
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/fordcan_ext.py",
|
||||
"sha256": "65ad2114db7210371fc5de06de962c0e29ce396a651c1dd2d58b07a80555a1bf",
|
||||
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
|
||||
},
|
||||
"fordcan": {
|
||||
"origin": "opendbc_repo/opendbc/car/ford/fordcan.py",
|
||||
"sha256": "0c35b8663afcaa3a58ab028a56f076c2c9f2f0296416c0abaf5905c674672956",
|
||||
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
|
||||
},
|
||||
"values": {
|
||||
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/values_ext.py",
|
||||
"sha256": "74802d82314eaa27eb610810d2815d3da77003c4a1c62e0437e57e785ba25f52",
|
||||
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
|
||||
},
|
||||
"starpilot_curvature_controller": {
|
||||
"origin": "opendbc_repo/opendbc/car/ford/carcontroller.py",
|
||||
"sha256": "c131d1d5f4231004e00f46968988b5d5f379ec22ef8c44d7f67ded8464344855",
|
||||
"revision": "d2ae98eb3959c88011ed537e7cbc033d91fd3efa"
|
||||
},
|
||||
"bpdev_trim": {
|
||||
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/lane_center_trim.py",
|
||||
"sha256": "1affc0abeaeeabaefca4741b0d75d8f249590c157340c5a9760a395d28f0a621",
|
||||
"revision": "e22afa6be9b881fa784c92ebb316db47728a3d81"
|
||||
}
|
||||
}
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
import math
|
||||
import numpy as np
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, apply_hysteresis, structs
|
||||
from opendbc.car.ford import fordcan
|
||||
from opendbc.car.ford.manual_turn import ManualTurnLatch
|
||||
from opendbc.car.ford.aol import qualified as qualified_aol
|
||||
from opendbc.car.ford.stock_cruise import FordStockCruiseButton, qualified as stock_switch_qualified
|
||||
from opendbc.car.ford.curvature_preview import blend_curvature
|
||||
from opendbc.car.ford import mache_can
|
||||
from opendbc.car.ford.extended_classic_can import create_extended_classic_lat_ctl_msg
|
||||
from opendbc.car.ford.classic_lateral import create_controller as create_classic_controller, bounded_command as classic_bounded_command
|
||||
from opendbc.car.ford.generic_canfd_can import create_extended_canfd_msg
|
||||
from opendbc.car.ford.generic_canfd_lateral import (
|
||||
GenericCanfdLateralController, qualified as generic_canfd_qualified, bounded_command as generic_canfd_bounded_command,
|
||||
)
|
||||
from opendbc.car.ford.mache_lateral import MachELateralController, FordLateralResult, bounded_command, qualified as mache_qualified
|
||||
from opendbc.car.ford.values import CarControllerParams, FordFlags, FordSafetyFlags, CAR
|
||||
from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
|
||||
|
||||
LongCtrlState = structs.CarControl.Actuators.LongControlState
|
||||
VisualAlert = structs.CarControl.HUDControl.VisualAlert
|
||||
|
||||
def anti_overshoot(apply_curvature, apply_curvature_last, v_ego):
|
||||
diff = 0.1
|
||||
tau = 5 # 5s smooths over the overshoot
|
||||
dt = DT_CTRL * CarControllerParams.STEER_STEP
|
||||
alpha = 1 - np.exp(-dt / tau)
|
||||
|
||||
lataccel = apply_curvature * (v_ego ** 2)
|
||||
last_lataccel = apply_curvature_last * (v_ego ** 2)
|
||||
last_lataccel = apply_hysteresis(lataccel, last_lataccel, diff)
|
||||
last_lataccel = alpha * lataccel + (1 - alpha) * last_lataccel
|
||||
|
||||
output_curvature = last_lataccel / (max(v_ego, 1) ** 2)
|
||||
|
||||
return float(np.interp(v_ego, [5, 10], [apply_curvature, output_curvature]))
|
||||
|
||||
|
||||
def apply_creep_compensation(accel: float, v_ego: float, car_fingerprint: str, *, standstill: bool, stopping: bool) -> float:
|
||||
if car_fingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not (standstill and stopping):
|
||||
return accel
|
||||
creep_accel = np.interp(v_ego, [1., 3.], [0.6, 0.])
|
||||
creep_accel = np.interp(accel, [0., 0.2], [creep_accel, 0.])
|
||||
accel -= creep_accel
|
||||
return float(accel)
|
||||
|
||||
|
||||
def apply_brake_rate_limit(accel: float, gas: float, accel_last: float, v_ego: float, car_fingerprint: str,
|
||||
*, stopping: bool, standstill: bool, fcw: bool) -> tuple[float, float]:
|
||||
mild_mach_e_braking = (
|
||||
car_fingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and
|
||||
-1.0 < gas < 0.0 and v_ego >= 5.0 and not (stopping or standstill or fcw)
|
||||
)
|
||||
if mild_mach_e_braking:
|
||||
accel = max(accel, min(accel_last, 0.0) - 2.0 * CarControllerParams.ACC_CONTROL_STEP * DT_CTRL)
|
||||
gas = accel
|
||||
else:
|
||||
accel = max(accel, accel_last - 3.5 * CarControllerParams.ACC_CONTROL_STEP * DT_CTRL)
|
||||
return float(accel), float(gas)
|
||||
|
||||
|
||||
class CarController(CarControllerBase):
|
||||
def __init__(self, dbc_names, CP):
|
||||
super().__init__(dbc_names, CP)
|
||||
self.packer = CANPacker(dbc_names[Bus.pt])
|
||||
self.CAN = fordcan.CanBus(CP)
|
||||
|
||||
self.manual_turn = ManualTurnLatch() if CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not CP.flags & FordFlags.LKA_STEERING else None
|
||||
self.stock_cruise_button = FordStockCruiseButton()
|
||||
self.manual_turn_inputs = None
|
||||
self.mache_lateral = MachELateralController(CP) if mache_qualified(CP) else None
|
||||
self.mache_extended_announced = False
|
||||
self.generic_canfd = generic_canfd_qualified(CP)
|
||||
self.classic_lateral = GenericCanfdLateralController(CP) if self.generic_canfd else create_classic_controller(CP)
|
||||
self.classic_extended_announced = False
|
||||
self.classic_profile = any(s.safetyModel == structs.CarParams.SafetyModel.ford and
|
||||
s.safetyParam & (FordSafetyFlags.CLASSIC_EXTENDED | FordSafetyFlags.GENERIC_CANFD_EXTENDED) for s in CP.safetyConfigs)
|
||||
self.mache_profile = any(s.safetyModel == structs.CarParams.SafetyModel.ford and
|
||||
s.safetyParam & FordSafetyFlags.MACH_E_EXTENDED for s in CP.safetyConfigs)
|
||||
self.apply_curvature_last = 0
|
||||
self.anti_overshoot_curvature_last = 0
|
||||
self.accel = 0.0
|
||||
self.gas = 0.0
|
||||
self.brake_request = False
|
||||
self.main_on_last = False
|
||||
self.lkas_enabled_last = False
|
||||
self.steer_alert_last = False
|
||||
self.lead_distance_bars_last = None
|
||||
self.distance_bar_frame = 0
|
||||
self.apply_lka_angle_last = 0.
|
||||
self.apply_lka_curvature_last = 0.
|
||||
|
||||
def update(self, CC, CS, now_nanos):
|
||||
can_sends = []
|
||||
if (self.mache_lateral is not None or self.classic_lateral is not None) and self.manual_turn_inputs is not None:
|
||||
self.manual_turn_inputs.update()
|
||||
self.manual_turn_inputs.apply_blend_settings(self)
|
||||
|
||||
actuators = CC.actuators
|
||||
hud_control = CC.hudControl
|
||||
|
||||
main_on = CS.out.cruiseState.available
|
||||
steer_alert = hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)
|
||||
fcw_alert = hud_control.visualAlert == VisualAlert.fcw
|
||||
|
||||
### acc buttons ###
|
||||
stock_cancel = False
|
||||
stock_resume = False
|
||||
if stock_switch_qualified(self.CP):
|
||||
stock_cancel, stock_resume = self.stock_cruise_button.update(
|
||||
bool(CS.buttons_stock_values["CcAslButtnCnclResPress"]),
|
||||
CS.out.cruiseState.available, CS.out.cruiseState.enabled)
|
||||
|
||||
if CC.cruiseControl.cancel:
|
||||
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, cancel=True))
|
||||
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, cancel=True))
|
||||
elif (stock_cancel or stock_resume) and (self.frame % CarControllerParams.BUTTONS_STEP) == 0:
|
||||
can_sends.append(fordcan.create_button_msg(
|
||||
self.packer, self.CAN.camera, CS.buttons_stock_values, cancel=stock_cancel, resume=stock_resume))
|
||||
can_sends.append(fordcan.create_button_msg(
|
||||
self.packer, self.CAN.main, CS.buttons_stock_values, cancel=stock_cancel, resume=stock_resume))
|
||||
elif CC.cruiseControl.resume and (self.frame % CarControllerParams.BUTTONS_STEP) == 0:
|
||||
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, resume=True))
|
||||
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, resume=True))
|
||||
# if stock lane centering isn't off, send a button press to toggle it off
|
||||
# the stock system checks for steering pressed, and eventually disengages cruise control
|
||||
elif CS.acc_tja_status_stock_values["Tja_D_Stat"] != 0 and (self.frame % CarControllerParams.ACC_UI_STEP) == 0:
|
||||
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, tja_toggle=True))
|
||||
|
||||
### lateral control ###
|
||||
# send steer msg at 20Hz
|
||||
if (self.frame % CarControllerParams.STEER_STEP) == 0 and self.CP.flags & FordFlags.LKA_STEERING:
|
||||
# Preserve the stock LMC heartbeat without requesting lateral action.
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, False, 0., 0., 0., 0.,
|
||||
stock_lmc=CS.lateral_motion_control))
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.mache_lateral is not None:
|
||||
permission = getattr(self.manual_turn_inputs, "assist_permission", None)
|
||||
self.mache_lateral.set_assist_permission(*(permission() if permission is not None else ((), False)))
|
||||
inputs = self.manual_turn_inputs.lateral_snapshot(CS.out.vEgoRaw) if self.manual_turn_inputs is not None else None
|
||||
if inputs is not None:
|
||||
self.mache_lateral.set_inputs(*inputs)
|
||||
else:
|
||||
# Withdraw stale preview while retaining current actuator/manual-turn control.
|
||||
self.mache_lateral.set_inputs(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)
|
||||
previous = self.mache_lateral.curvature_last
|
||||
previous_path_angle = self.mache_lateral.path_angle_last
|
||||
demanded = self.mache_lateral.update(CC, CS, actuators) if self.mache_extended_announced else FordLateralResult()
|
||||
lateral = bounded_command(self.mache_lateral, demanded, previous, CS.out.vEgoRaw, previous_path_angle)
|
||||
self.mache_lateral_demand = demanded
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(mache_can.create_lat_ctl2_msg(
|
||||
self.packer, self.CAN, int(lateral.active), lateral.ramp_type, lateral.precision_type,
|
||||
-lateral.curvature, -lateral.curvature_rate, counter, -lateral.path_angle))
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.classic_lateral is not None:
|
||||
inputs = self.manual_turn_inputs.lateral_snapshot(CS.out.vEgoRaw) if self.manual_turn_inputs is not None else None
|
||||
if inputs is not None:
|
||||
self.classic_lateral.set_inputs(*inputs)
|
||||
else:
|
||||
self.classic_lateral.set_inputs(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)
|
||||
previous = self.classic_lateral.curvature_last
|
||||
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced else FordLateralResult()
|
||||
bound = generic_canfd_bounded_command if self.generic_canfd else classic_bounded_command
|
||||
lateral = bound(self.classic_lateral, demanded, previous, CS.out.vEgoRaw,
|
||||
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
|
||||
self.classic_lateral_demand = demanded
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
if self.generic_canfd:
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(create_extended_canfd_msg(
|
||||
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
|
||||
-lateral.curvature, -lateral.curvature_rate, counter))
|
||||
else:
|
||||
can_sends.append(create_extended_classic_lat_ctl_msg(
|
||||
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
|
||||
-lateral.curvature, -lateral.curvature_rate))
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0:
|
||||
lateral_active = CC.latActive and not self.mache_profile and not self.classic_profile
|
||||
manual_turn = False
|
||||
if self.manual_turn is not None and self.manual_turn_inputs is not None:
|
||||
detection_enabled, lane_change, model_ready = self.manual_turn_inputs.update()
|
||||
manual_turn = self.manual_turn.update(CC, CS, float(actuators.curvature), detection_enabled, lane_change, model_ready)
|
||||
if manual_turn:
|
||||
lateral_active = False
|
||||
self.apply_curvature_last = 0.
|
||||
# Bronco and some other cars consistently overshoot curv requests
|
||||
# Apply some deadzone + smoothing convergence to avoid oscillations
|
||||
if self.CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_F_150_MK14):
|
||||
self.anti_overshoot_curvature_last = anti_overshoot(actuators.curvature, self.anti_overshoot_curvature_last, CS.out.vEgoRaw)
|
||||
apply_curvature = self.anti_overshoot_curvature_last
|
||||
else:
|
||||
apply_curvature = actuators.curvature
|
||||
|
||||
# apply rate limits, curvature error limit, and clip to signal range
|
||||
current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
if self.manual_turn is not None and self.manual_turn_inputs is not None and lateral_active:
|
||||
prediction = getattr(self.manual_turn_inputs, "preview_curvature", None)
|
||||
preview = prediction(CS.out.vEgoRaw) if prediction is not None else None
|
||||
if preview is not None:
|
||||
apply_curvature = blend_curvature(apply_curvature, preview, current_curvature)
|
||||
# No blending at low speed due to lack of torque wind-up and inaccurate current curvature
|
||||
if CS.out.vEgoRaw > 9:
|
||||
apply_curvature = float(np.clip(apply_curvature, current_curvature - CarControllerParams.CURVATURE_ERROR,
|
||||
current_curvature + CarControllerParams.CURVATURE_ERROR))
|
||||
apply_curvature = CarControllerParams.CURVATURE_LIMITS.apply_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw,
|
||||
0., lateral_active, CarControllerParams.STEER_STEP)
|
||||
self.apply_curvature_last = 0. if manual_turn else apply_curvature
|
||||
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
# TODO: extended mode
|
||||
# Ford uses four individual signals to dictate how to drive to the car. Curvature alone (limited to 0.02 m^-1)
|
||||
# can actuate the steering for a large portion of any lateral movements. However, in order to get further control on
|
||||
# steer actuation, the other three signals are necessary. Ford controls vehicles differently than most other makes.
|
||||
# A detailed explanation on ford control can be found here:
|
||||
# https://www.f150gen14.com/forum/threads/introducing-bluepilot-a-ford-specific-fork-for-comma3x-openpilot.24241/#post-457706
|
||||
mode = 1 if lateral_active else 0
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -self.apply_curvature_last, 0., counter))
|
||||
else:
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, lateral_active, 0., 0., -self.apply_curvature_last, 0.))
|
||||
|
||||
# send lka msg at 33Hz
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0:
|
||||
if self.CP.flags & FordFlags.LKA_STEERING:
|
||||
source_age_ns = now_nanos - CS.lkas_available_ts_nanos
|
||||
lka_active = (CC.latActive and CS.lkas_available and 0 <= source_age_ns <= 100_000_000 and
|
||||
(CS.out.cruiseState.enabled or qualified_aol(self.CP, marked_only=True)) and
|
||||
not CS.out.steerFaultTemporary and
|
||||
not CS.out.vehicleSensorsInvalid)
|
||||
if lka_active:
|
||||
desired_angle = float(np.clip(actuators.steeringAngleDeg - CS.out.steeringAngleDeg, -5.8, 5.8))
|
||||
self.apply_lka_angle_last = float(np.clip(desired_angle, self.apply_lka_angle_last - 1.,
|
||||
self.apply_lka_angle_last + 1.))
|
||||
desired_curvature = -actuators.curvature
|
||||
current_curvature = CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
if CS.out.vEgoRaw > 9.:
|
||||
desired_curvature = float(np.clip(desired_curvature, current_curvature - CarControllerParams.CURVATURE_ERROR,
|
||||
current_curvature + CarControllerParams.CURVATURE_ERROR))
|
||||
self.apply_lka_curvature_last = CarControllerParams.LKA_CURVATURE_LIMITS.apply_limits(
|
||||
desired_curvature, self.apply_lka_curvature_last, CS.out.vEgoRaw, 0., True, CarControllerParams.LKA_STEP)
|
||||
else:
|
||||
self.apply_lka_angle_last = 0.
|
||||
self.apply_lka_curvature_last = 0.
|
||||
self.apply_curvature_last = -self.apply_lka_curvature_last
|
||||
direction = 2 if self.apply_lka_angle_last >= 0. else 4
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN, lka_active, self.apply_lka_angle_last,
|
||||
direction, self.apply_lka_curvature_last, transit=True))
|
||||
elif self.mache_lateral is not None:
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.mache_extended_announced = True
|
||||
elif self.classic_lateral is not None:
|
||||
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
|
||||
self.classic_extended_announced = True
|
||||
else:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
|
||||
### longitudinal control ###
|
||||
# send acc msg at 50Hz
|
||||
if self.CP.openpilotLongitudinalControl and (self.frame % CarControllerParams.ACC_CONTROL_STEP) == 0:
|
||||
accel = actuators.accel
|
||||
gas = accel
|
||||
stopping = actuators.longControlState == LongCtrlState.stopping
|
||||
|
||||
if CC.longActive:
|
||||
accel = apply_creep_compensation(accel, CS.out.vEgo, self.CP.carFingerprint,
|
||||
standstill=CS.out.standstill, stopping=stopping)
|
||||
|
||||
accel, gas = apply_brake_rate_limit(
|
||||
accel, gas, self.accel, CS.out.vEgo, self.CP.carFingerprint,
|
||||
stopping=stopping, standstill=CS.out.standstill,
|
||||
fcw=fcw_alert or CS.out.stockFcw or CS.out.stockAeb,
|
||||
)
|
||||
|
||||
accel = float(np.clip(accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX))
|
||||
gas = float(np.clip(gas, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX))
|
||||
|
||||
# Both gas and accel are in m/s^2, accel is used solely for braking
|
||||
if not CC.longActive or gas < CarControllerParams.MIN_GAS:
|
||||
gas = CarControllerParams.INACTIVE_GAS
|
||||
|
||||
# PCM applies pitch compensation to gas/accel, but we need to compensate for the brake/pre-charge bits
|
||||
accel_due_to_pitch = 0.0
|
||||
if len(CC.orientationNED) == 3:
|
||||
accel_due_to_pitch = math.sin(CC.orientationNED[1]) * ACCELERATION_DUE_TO_GRAVITY
|
||||
|
||||
accel_pitch_compensated = accel + accel_due_to_pitch
|
||||
if accel_pitch_compensated > 0.3 or not CC.longActive:
|
||||
self.brake_request = False
|
||||
elif accel_pitch_compensated < 0.0:
|
||||
self.brake_request = True
|
||||
|
||||
# TODO: look into using the actuators packet to send the desired speed
|
||||
can_sends.append(fordcan.create_acc_msg(self.packer, self.CAN, CC.longActive, gas, accel, stopping, self.brake_request, v_ego_kph=V_CRUISE_MAX))
|
||||
|
||||
self.accel = accel
|
||||
self.gas = gas
|
||||
|
||||
### ui ###
|
||||
send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != CC.latActive) or (self.steer_alert_last != steer_alert)
|
||||
# send lkas ui msg at 1Hz or if ui state changes
|
||||
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
|
||||
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values))
|
||||
|
||||
# send acc ui msg at 5Hz or if ui state changes
|
||||
if hud_control.leadDistanceBars != self.lead_distance_bars_last:
|
||||
send_ui = True
|
||||
self.distance_bar_frame = self.frame
|
||||
|
||||
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
|
||||
show_distance_bars = self.frame - self.distance_bar_frame < 400
|
||||
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive,
|
||||
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
|
||||
hud_control, CS.acc_tja_status_stock_values))
|
||||
|
||||
self.main_on_last = main_on
|
||||
self.lkas_enabled_last = CC.latActive
|
||||
self.steer_alert_last = steer_alert
|
||||
self.lead_distance_bars_last = hud_control.leadDistanceBars
|
||||
|
||||
new_actuators = actuators.as_builder()
|
||||
new_actuators.curvature = self.apply_curvature_last
|
||||
if self.CP.flags & FordFlags.LKA_STEERING:
|
||||
new_actuators.steeringAngleDeg = CS.out.steeringAngleDeg + self.apply_lka_angle_last
|
||||
new_actuators.accel = self.accel
|
||||
new_actuators.gas = self.gas
|
||||
|
||||
self.frame += 1
|
||||
return new_actuators, can_sends
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
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 import namedtuple
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.docs_definitions import CarParts, Device
|
||||
from opendbc.car.ford.values import CAR
|
||||
from opendbc.car.lateral import AngleSteeringLimits
|
||||
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
Button = namedtuple('Button', ['event_type', 'can_addr', 'can_msg', 'values'])
|
||||
|
||||
# Ford cruise control buttons are in the Steering_Data_FD1 message (CAN ID 131)
|
||||
# These signals are 1-bit flags: 1 = pressed, 0 = not pressed
|
||||
#
|
||||
# Note: Some buttons are combo buttons that emit multiple ButtonEvent types:
|
||||
# - CcAslButtnSetIncPress emits both accelCruise (type 3) and setCruise (type 9)
|
||||
# - CcAslButtnCnclResPress emits both cancel (type 5) and resumeCruise (type 10)
|
||||
#
|
||||
# There is also a separate CcAslButtnSetPress signal for the standalone "Set" button,
|
||||
# but based on user mapping, setCruise is mapped to the combo button instead.
|
||||
BUTTONS = [
|
||||
# Combo button: Set + Increase (emits accelCruise when enabled, setCruise when disabled)
|
||||
Button(ButtonType.accelCruise, "Steering_Data_FD1", "CcAslButtnSetIncPress", [1]),
|
||||
Button(ButtonType.setCruise, "Steering_Data_FD1", "CcAslButtnSetIncPress", [1]),
|
||||
|
||||
# Combo button: Set + Decrease (emits decelCruise when enabled, setCruise when disabled)
|
||||
Button(ButtonType.decelCruise, "Steering_Data_FD1", "CcAslButtnSetDecPress", [1]),
|
||||
Button(ButtonType.setCruise, "Steering_Data_FD1", "CcAslButtnSetDecPress", [1]),
|
||||
|
||||
# Combo button: Cancel/Resume (emits cancel when enabled, resumeCruise when disabled)
|
||||
Button(ButtonType.cancel, "Steering_Data_FD1", "CcAslButtnCnclResPress", [1]),
|
||||
Button(ButtonType.resumeCruise, "Steering_Data_FD1", "CcAslButtnCnclResPress", [1]),
|
||||
|
||||
# Main cruise button (on/off toggle)
|
||||
Button(ButtonType.mainCruise, "Steering_Data_FD1", "CcButtnOnOffPress", [1]),
|
||||
]
|
||||
|
||||
|
||||
class FordSafetyFlagsSP:
|
||||
"""Sunnypilot-level safety flags for Ford.
|
||||
|
||||
Carried in CP_SP.safetyParam and delivered to the safety firmware as
|
||||
current_safety_param_sp (the separate SP uint16, USB control 0xdf) -- NOT the main
|
||||
safetyConfigs[].safetyParam. ford_init reads it with GET_FLAG(current_safety_param_sp,
|
||||
...), same pattern as Subaru STOP_AND_GO (subaru_common.h). Plain int constants, not
|
||||
IntFlag: CP_SP.safetyParam must stay a plain int through capnp serialization in card.
|
||||
"""
|
||||
STEER_ANGLE_CURVATURE = 1
|
||||
|
||||
|
||||
# Geometry-table index for the steering-angle curvature measurement, packed into
|
||||
# CP_SP.safetyParam bits 1-4 when STEER_ANGLE_CURVATURE is set. Must match the
|
||||
# ford_pinion_geometry table in safety/modes/ford.h row for row (enforced by
|
||||
# test_ford.py's geometry-consistency test against CarSpecs + calc_slip_factor).
|
||||
# Index 0 is reserved as invalid: the firmware treats flag-set-but-no-index as feature
|
||||
# off, so a half-configured param can never select the wrong geometry silently.
|
||||
# FORD_EDGE_MK2 is deliberately absent: ALT_STEER_ANGLE platforms read a RELATIVE pinion
|
||||
# angle (SteeringPinion_Data_Alt + learned offset) and lack the absolute measurement
|
||||
# this feature needs -- the toggle no-ops there and yaw behavior is kept.
|
||||
FORD_PINION_GEOMETRY_SHIFT = 1
|
||||
FORD_PINION_GEOMETRY_INDEX = {
|
||||
CAR.FORD_BRONCO_SPORT_MK1: 1,
|
||||
CAR.FORD_ESCAPE_MK4: 2,
|
||||
CAR.FORD_ESCAPE_MK4_5: 3,
|
||||
CAR.FORD_EXPEDITION_MK4: 4,
|
||||
CAR.FORD_EXPLORER_MK6: 5,
|
||||
CAR.FORD_FOCUS_MK4: 6,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1: 7,
|
||||
CAR.FORD_F_150_MK14: 8,
|
||||
CAR.FORD_MAVERICK_MK1: 9,
|
||||
CAR.FORD_MONDEO_MK5: 10,
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1: 11,
|
||||
CAR.FORD_RANGER_MK2: 12,
|
||||
}
|
||||
|
||||
|
||||
# BluePilot: Max curvature for steering command (m^-1), from DBC file limits
|
||||
CURVATURE_MAX = 0.02
|
||||
|
||||
# BluePilot: Curvature rate limits — 3-point breakpoints for smoother lateral control.
|
||||
# Upstream opendbc uses 2-point ([5, 25]) with more conservative values.
|
||||
# These allow higher rates at low speed for responsiveness, lower rates at mid-speed
|
||||
# for comfort, and very low rates at highway speed for stability.
|
||||
#
|
||||
# Control (Python) uses stricter windup than unwind so OP stays inside panda when apply_std
|
||||
# picks the wrong table vs steer_angle_cmd_checks. Safety firmware uses looser symmetric ROCs
|
||||
# (former “down” table for both up/down) — see ford.h FORD_LIMITS.
|
||||
# Tests: test_ford.py ANGLE_RATE_* match ford.h, not the stricter BP_ANGLE_LIMITS up row.
|
||||
_BP_ANGLE_RATE_UP = ([5, 16, 25], [0.0025, 0.0012, 0.00008])
|
||||
_BP_ANGLE_RATE_DOWN = ([5, 16, 25], [0.0025, 0.0014, 0.00018])
|
||||
BP_ANGLE_LIMITS = AngleSteeringLimits(
|
||||
0.02, # Max curvature for steering command, m^-1
|
||||
_BP_ANGLE_RATE_UP,
|
||||
_BP_ANGLE_RATE_DOWN,
|
||||
)
|
||||
|
||||
|
||||
def apply_bp_device_mount(car_docs, CP):
|
||||
"""BluePilot: Select comma3 mount type per vehicle.
|
||||
|
||||
Most Ford vehicles use the angled mount due to windshield angle.
|
||||
Vehicles not in this list use the standard mount.
|
||||
"""
|
||||
from opendbc.car.ford.values import CAR, CarHarness, FordFlags
|
||||
harness = CarHarness.ford_q4 if CP.flags & FordFlags.CANFD else CarHarness.ford_q3
|
||||
if CP.carFingerprint in (
|
||||
CAR.FORD_BRONCO_SPORT_MK1,
|
||||
CAR.FORD_MAVERICK_MK1,
|
||||
CAR.FORD_F_150_MK14,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1,
|
||||
CAR.FORD_ESCAPE_MK4_5,
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1,
|
||||
CAR.FORD_RANGER_MK2,
|
||||
CAR.FORD_EDGE_MK2,
|
||||
):
|
||||
car_docs.car_parts = CarParts([Device.threex_angled_mount, harness])
|
||||
else:
|
||||
car_docs.car_parts = CarParts([Device.threex, harness])
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
"""Pure angle host-independent finite inputs and explicit override/blip reset."""
|
||||
import math
|
||||
import unittest
|
||||
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs, Command
|
||||
from opendbc.bluepilot_lateral.params import limits
|
||||
from opendbc.bluepilot_lateral.tests import reference
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import CLASSIC, CANFD, TRUCKS
|
||||
from opendbc.bluepilot_lateral.can.ford import lateral, lka
|
||||
from opendbc.can import CANPacker
|
||||
from types import SimpleNamespace
|
||||
from dataclasses import replace
|
||||
from opendbc.bluepilot_lateral.core.trim import LaneCenterTrim, LaneGeometry, TrimConfig
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import model_geometry
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
|
||||
class TestAngleCore(unittest.TestCase):
|
||||
def test_finite_permission_and_inactive_state_clear_pending_history(self):
|
||||
for value in (math.nan, math.inf, -math.inf):
|
||||
for field in ("speed", "desired_curvature", "measured_curvature", "steering_angle", "lateral_delay"):
|
||||
core = AngleController()
|
||||
inputs = Inputs(active=True, speed=15., measured_curvature=0., desired_curvature=.001)
|
||||
self.assertEqual(core.step(replace(inputs, **{field: value})), Command())
|
||||
core = AngleController()
|
||||
for _ in range(10):
|
||||
self.assertTrue(core.step(Inputs(True, 15., 0., .001)).active)
|
||||
self.assertEqual(core.step(Inputs(False, 15., 0., .001)), Command())
|
||||
self.assertEqual(core.path_last, 0.)
|
||||
|
||||
def test_actual_equivalent_jerk_and_cap_both_signs_and_speeds(self):
|
||||
for speed in (1., 9., 10., 15., 25., 40., 55.):
|
||||
for direction in (-1, 1):
|
||||
core = AngleController(low_gain=.95, high_gain=.95)
|
||||
previous = 0.
|
||||
for _ in range(60):
|
||||
command = core.step(Inputs(True, speed, 0., direction * .002))
|
||||
if command.active:
|
||||
equivalent = command.path_angle / speed / limits.GAIN_MIN
|
||||
self.assertLessEqual(abs(equivalent), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
|
||||
self.assertLessEqual(abs(equivalent - previous), limits.LATERAL_JERK / max(speed - 1., 1.) ** 2 * .05)
|
||||
previous = equivalent
|
||||
else:
|
||||
previous = 0.
|
||||
|
||||
def test_human_turn_and_release_blips_are_neutral_then_bounded(self):
|
||||
core = AngleController()
|
||||
for _ in range(61):
|
||||
result = core.step(Inputs(True, 15., 0., .001, steering_pressed=True, steering_angle=60.))
|
||||
self.assertFalse(result.active)
|
||||
self.assertEqual(result.path_angle, 0.)
|
||||
core = AngleController()
|
||||
for _ in range(12):
|
||||
core.step(Inputs(True, 15., 0., .001, steering_pressed=True))
|
||||
for _ in range(6):
|
||||
self.assertEqual(core.step(Inputs(True, 15., 0., .001)), Command())
|
||||
resumed = core.step(Inputs(True, 15., 0., .001))
|
||||
self.assertTrue(resumed.active)
|
||||
self.assertLess(abs(resumed.path_angle), .02)
|
||||
|
||||
def test_pinned_bp7_pure_angle_and_actual_control_bytes_with_documented_limits(self):
|
||||
builders = reference.builders()
|
||||
bus = SimpleNamespace(main=0)
|
||||
differences = set()
|
||||
packer = CANPacker("ford_lincoln_base_pt")
|
||||
for identity in sorted(CLASSIC | CANFD):
|
||||
gains = (.95, .95) if identity in TRUCKS else ((1., 1.05) if identity in CANFD else (1., 1.15))
|
||||
for speed in (5., 15., 25., 40.):
|
||||
for direction in (-1, 1):
|
||||
adapted, upstream = AngleController(low_gain=gains[0], high_gain=gains[1]), reference.Angle(identity)
|
||||
for tick in range(60):
|
||||
# Tiny tracking curves stay clear of all intentional envelope changes.
|
||||
desired = direction * .00005 * (1. + .2 * math.sin(tick / 8.))
|
||||
inputs = Inputs(True, speed, desired, desired,
|
||||
yaw_predictions=tuple(desired * speed for _ in ModelConstants.T_IDXS),
|
||||
time_indices=tuple(ModelConstants.T_IDXS), lateral_delay=.15,
|
||||
lane_change=tick >= 30, lane_direction=1 if direction < 0 else 2)
|
||||
result = upstream.step(inputs)
|
||||
command = adapted.step(inputs)
|
||||
self.assertTrue(command.active)
|
||||
self.assertAlmostEqual(command.path_angle, result.path_angle, places=12)
|
||||
self.assertEqual(command.precision, result.precision_type)
|
||||
fd = identity in CANFD
|
||||
actual = lateral(packer, 0, command, canfd=fd, counter=tick % 16)
|
||||
expected = (builders["create_lat_ctl2_msg"](packer, bus, 1, result.ramp_type, result.precision_type,
|
||||
0., -result.path_angle, 0., 0., tick % 16) if fd else
|
||||
builders["create_lat_ctl_msg"](packer, bus, True, result.ramp_type, result.precision_type,
|
||||
0., -result.path_angle, 0., 0.))
|
||||
self.assertEqual(actual, expected)
|
||||
old_shadow = builders["create_lka_msg"](packer, bus, True, SimpleNamespace(), True, upstream.controller.bp_kappa_cmd)
|
||||
new_shadow = lka(packer, 0, command.shadow_curvature, tick % 8)
|
||||
# bp7 packs an empty body as raw zeros; the strict native contract requires physical zeros.
|
||||
self.assertEqual(old_shadow[1][:4], b"\x00\x00\x00\x00")
|
||||
self.assertEqual(new_shadow[1][:4], b"\x00\x80\x08\x00")
|
||||
differences.update(("physical_neutral_dbc_offsets", "versioned_side_channel"))
|
||||
# Aggressive entry demonstrates the required stricter native-equivalent derivative/cap.
|
||||
adapted, upstream = AngleController(low_gain=gains[0], high_gain=gains[1]), reference.Angle(identity)
|
||||
previous = 0.
|
||||
for _tick in range(8):
|
||||
inputs = Inputs(True, speed, 0., direction * .02,
|
||||
yaw_predictions=tuple(direction * .02 * speed for _ in ModelConstants.T_IDXS),
|
||||
time_indices=tuple(ModelConstants.T_IDXS), lateral_delay=.15)
|
||||
result, command = upstream.step(inputs), adapted.step(inputs)
|
||||
if command.active:
|
||||
equivalent = command.path_angle / max(speed, .1) / limits.GAIN_MIN
|
||||
self.assertLessEqual(abs(equivalent), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
|
||||
self.assertLessEqual(abs(equivalent - previous), limits.LATERAL_JERK / max(speed - 1., 1.) ** 2 * .05)
|
||||
previous = equivalent
|
||||
if abs(command.path_angle - result.path_angle) > 1e-7:
|
||||
differences.add("native_cap_jerk_deviation_packing")
|
||||
else:
|
||||
previous = 0.
|
||||
self.assertEqual(differences, {"physical_neutral_dbc_offsets", "versioned_side_channel", "native_cap_jerk_deviation_packing"})
|
||||
|
||||
|
||||
class TestAngleTrim(unittest.TestCase):
|
||||
@staticmethod
|
||||
def geometry(direction=1., *, width=3.3, probability=.9):
|
||||
xs = (0., 10., 40.)
|
||||
return LaneGeometry(xs, (direction * .3,) * 3, xs, (-width / 2,) * 3,
|
||||
xs, (width / 2,) * 3, (probability,) * 2, (.1,) * 2, True)
|
||||
|
||||
@staticmethod
|
||||
def model(g):
|
||||
return SimpleNamespace(position=SimpleNamespace(x=g.position_x, y=g.position_y),
|
||||
laneLines=[SimpleNamespace(x=(), y=()), SimpleNamespace(x=g.left_x, y=g.left_y),
|
||||
SimpleNamespace(x=g.right_x, y=g.right_y)],
|
||||
laneLineProbs=[0., *g.probabilities], laneLineStds=[0., *g.standard_deviations])
|
||||
|
||||
def test_disabled_trim_preserves_all_eleven_default_commands_and_wire_bytes(self):
|
||||
packer = CANPacker("ford_lincoln_base_pt")
|
||||
for identity in sorted(CLASSIC | CANFD):
|
||||
low, high = (.95, .95) if identity in TRUCKS else ((1., 1.05) if identity in CANFD else (1., 1.15))
|
||||
for speed in (5., 15., 25., 40.):
|
||||
for direction in (-1, 1):
|
||||
baseline, supplied = AngleController(low_gain=low, high_gain=high), AngleController(low_gain=low, high_gain=high)
|
||||
self.assertFalse(supplied.trim_config.enabled)
|
||||
for tick in range(30):
|
||||
i = Inputs(True, speed, 0., direction * .0002)
|
||||
a, b = baseline.step(i), supplied.step(replace(i, geometry=self.geometry(direction)))
|
||||
self.assertEqual(a, b)
|
||||
self.assertEqual(lateral(packer, 0, a, canfd=identity in CANFD, counter=tick % 16),
|
||||
lateral(packer, 0, b, canfd=identity in CANFD, counter=tick % 16))
|
||||
self.assertEqual(lka(packer, 0, a.shadow_curvature, tick % 8), lka(packer, 0, b.shadow_curvature, tick % 8))
|
||||
self.assertEqual(supplied.trim.correction, 0.)
|
||||
|
||||
def test_fresh_geometry_matches_exact_currentdev_trim_and_model_fallback(self):
|
||||
for speed in (5., 9., 12., 15., 25., 40.):
|
||||
for direction in (-1, 1):
|
||||
adapted, upstream = LaneCenterTrim(), reference.trim()()
|
||||
config = TrimConfig(True, direction * .1, .25)
|
||||
for tick in range(90):
|
||||
g = self.geometry(direction, width=3.3 if tick < 30 or tick >= 60 else 1.5,
|
||||
probability=.9 if tick < 60 else .3)
|
||||
actual = adapted.update(.0001, speed, g, config, active=True, lane_change=False)
|
||||
expected = upstream.update(.0001, self.model(g), speed, True, config.offset_m, config.gain, True, False)
|
||||
self.assertAlmostEqual(actual, expected, places=14)
|
||||
self.assertAlmostEqual(adapted.correction, upstream.correction, places=14)
|
||||
projected = model_geometry(self.model(g), fresh=True)
|
||||
self.assertEqual(projected, g)
|
||||
self.assertIsNone(model_geometry(self.model(g), fresh=False))
|
||||
|
||||
def test_invalid_stale_driver_and_lane_change_suppress_trim_and_reset_history(self):
|
||||
original = self.geometry()
|
||||
config = TrimConfig(True, 0., .25)
|
||||
for geometry in (replace(original, fresh=False), replace(original, position_x=(0., 0., 40.)),
|
||||
replace(original, position_y=(0., math.nan, 0.)), replace(original, position_y=(0.,)), None):
|
||||
trim = LaneCenterTrim()
|
||||
self.assertNotEqual(trim.update(0., 15., original, config, active=True, lane_change=False), 0.)
|
||||
self.assertEqual(trim.update(.001, 15., geometry, config, active=True, lane_change=False), .001)
|
||||
self.assertEqual(trim.correction, 0.)
|
||||
for changes in ({"active": False}, {"lane_change": True}, {"steering_pressed": True}):
|
||||
trim = LaneCenterTrim()
|
||||
trim.update(0., 15., original, config, active=True, lane_change=False)
|
||||
flags = dict(active=True, lane_change=False, steering_pressed=False)
|
||||
flags.update(changes)
|
||||
self.assertEqual(trim.update(.001, 15., original, config, **flags), .001)
|
||||
self.assertEqual(trim.correction, 0.)
|
||||
for invalid in (TrimConfig(True, math.nan, .25), TrimConfig(True, 0., math.inf), TrimConfig(True, .6, .25)):
|
||||
self.assertEqual(LaneCenterTrim().update(.001, 15., original, invalid, active=True, lane_change=False), .001)
|
||||
# Bad lane-only references fall back to the model+offset, not a stale lane correction.
|
||||
bad = replace(original, probabilities=(math.nan, .9))
|
||||
without_lines = replace(original, left_x=(), right_x=())
|
||||
a, b = LaneCenterTrim(), LaneCenterTrim()
|
||||
self.assertEqual(a.update(0., 15., bad, TrimConfig(True, .1, .25), active=True, lane_change=False),
|
||||
b.update(0., 15., without_lines, TrimConfig(True, .1, .25), active=True, lane_change=False))
|
||||
core = AngleController(trim_config=config)
|
||||
core.step(Inputs(True, 15., 0., 0., geometry=original))
|
||||
self.assertNotEqual(core.trim.correction, 0.)
|
||||
self.assertEqual(core.step(Inputs(False, 15., 0., 0., geometry=original)), Command())
|
||||
self.assertEqual(core.trim.correction, 0.)
|
||||
|
||||
def test_enabled_trim_remains_inside_final_curvature_jerk_and_shadow_bounds(self):
|
||||
for speed in (9., 10., 15., 25., 40.):
|
||||
for direction in (-1, 1):
|
||||
core = AngleController(low_gain=.95, high_gain=.95, trim_config=TrimConfig(True, direction * .5, 1.))
|
||||
previous, previous_trim, previous_active = 0., 0., False
|
||||
for _tick in range(100):
|
||||
command = core.step(Inputs(True, speed, 0., direction * .02, geometry=self.geometry(-direction)))
|
||||
if command.active and previous_active:
|
||||
self.assertLessEqual(abs(core.trim.correction - previous_trim), .00015 + 1e-12)
|
||||
previous_trim, previous_active = core.trim.correction, command.active
|
||||
if command.active:
|
||||
equivalent = command.path_angle / speed / limits.GAIN_MIN
|
||||
self.assertLessEqual(abs(equivalent), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
|
||||
self.assertLessEqual(abs(equivalent - previous), limits.LATERAL_JERK / max(speed - 1., 1.) ** 2 * .05)
|
||||
self.assertLessEqual(abs(command.shadow_curvature), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
|
||||
# Shadow comes from the final path; no independent trim/shadow command can bypass bounds.
|
||||
ratio = command.path_angle / (speed * command.shadow_curvature) if command.shadow_curvature else 1.
|
||||
self.assertTrue(limits.GAIN_MIN - 1e-12 <= ratio <= limits.GAIN_MAX + 1e-12)
|
||||
previous = equivalent
|
||||
else:
|
||||
previous = 0.
|
||||
self.assertEqual(core.trim.correction, 0.)
|
||||
@@ -0,0 +1,307 @@
|
||||
"""Public packed angle/native boundaries; uses the runner-selected safety library."""
|
||||
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs, Command
|
||||
from opendbc.bluepilot_lateral.can.ford import lateral, lka, checksum4
|
||||
from opendbc.safety.tests import common
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.can import CANPacker
|
||||
|
||||
|
||||
class TestFordAngleNative(common.SafetyTestBase):
|
||||
DBC = "ford_lincoln_base_pt"
|
||||
SAFETY_MODEL = CarParams.SafetyModel.ford
|
||||
SAFETY_PARAM = 128
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.debug = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) == 0
|
||||
self.safety.set_safety_hooks(self.SAFETY_MODEL, self.SAFETY_PARAM)
|
||||
self.safety.init_tests()
|
||||
self.now = 1_000_000
|
||||
self.lka_count = 0
|
||||
self.control_count = 0
|
||||
self.word = self.SAFETY_PARAM
|
||||
|
||||
def packet(self, msg):
|
||||
addr, dat, bus = msg
|
||||
return libsafety_py.make_CANPacket(addr, bus, dat)
|
||||
|
||||
def rx(self, name, values, *, bus=0):
|
||||
self.assertTrue(self._rx(self.packer.make_can_msg_safety(name, bus, values)))
|
||||
|
||||
def warm(self, speed=15.0, curvature=0.0):
|
||||
self.safety.set_timer(self.now)
|
||||
for _ in range(6):
|
||||
self.rx("BrakeSysFeatures", {"Veh_V_ActlBrk": speed * 3.6, "VehVActlBrk_D_Qf": 3})
|
||||
self.rx("EngVehicleSpThrottle2", {"Veh_V_ActlEng": speed * 3.6, "VehVActlEng_D_Qf": 3})
|
||||
self.rx("Yaw_Data_FD1", {"VehYaw_W_Actl": curvature * speed, "VehYawWActl_D_Qf": 3})
|
||||
self.rx("EngVehicleSpThrottle", {"ApedPos_Pc_ActlArb": 0})
|
||||
self.rx("DesiredTorqBrk", {"VehStop_D_Stat": 0})
|
||||
self.rx("Steering_Data_FD1", {"CcAslButtnCnclResPress": 0})
|
||||
if not self.safety.get_controls_allowed():
|
||||
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 0})
|
||||
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 5})
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
|
||||
def reset_word(self, word):
|
||||
self.safety.set_alternative_experience(0)
|
||||
self.safety.set_safety_hooks(self.SAFETY_MODEL, word)
|
||||
self.word = word
|
||||
self.lka_count = self.control_count = 0
|
||||
self.now += 1_000_000
|
||||
self.warm()
|
||||
|
||||
def shadow(self, command):
|
||||
msg = lka(self.packer, 0, command.shadow_curvature, self.lka_count)
|
||||
self.lka_count = (self.lka_count + 1) % 8
|
||||
return msg
|
||||
|
||||
def control(self, command):
|
||||
msg = lateral(self.packer, 0, command, canfd=bool(self.word & 2), counter=self.control_count)
|
||||
self.control_count = (self.control_count + 1) % 16
|
||||
return msg
|
||||
|
||||
def send(self, msg, expected=True):
|
||||
self.assertEqual(bool(self._tx(self.packet(msg))), expected)
|
||||
|
||||
def tick(self, command):
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
|
||||
def test_real_packer_physical_neutral_shadow_and_corruptions(self):
|
||||
packer = CANPacker(self.DBC)
|
||||
for word in (128, 130):
|
||||
for direction in (-1, 1):
|
||||
for fault in ("angle", "curvature", "checksum", "version", "counter", "replay"):
|
||||
with self.subTest(word=word, direction=direction, fault=fault):
|
||||
self.reset_word(word)
|
||||
command = Command(True, direction * 0.005, direction * 0.005 / 15.0)
|
||||
# Real RX and PCM edges in warm() establish permission, with no allowance setter.
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
good = lka(packer, 0, command.shadow_curvature, 0)
|
||||
addr, data, bus = good
|
||||
self.assertEqual((addr, bus), (0x3CA, 0))
|
||||
self.assertEqual(data[:4], b"\x00\x80\x08\x00")
|
||||
self.assertEqual(((data[2] & 15) << 8) | data[3], 2048)
|
||||
self.assertEqual((data[1] << 4) | (data[2] >> 4), 2048)
|
||||
self.assertEqual(data[7] & 15, 1)
|
||||
self.assertEqual(data[7] >> 4, checksum4(data))
|
||||
self.assertEqual(int.from_bytes(data[5:7], "big", signed=True), round(-command.shadow_curvature * 1e6))
|
||||
self.send(good)
|
||||
self.send(lateral(packer, 0, command, canfd=bool(word & 2), counter=0))
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
next_good = lka(packer, 0, command.shadow_curvature, 1)
|
||||
mutated = bytearray(next_good[1])
|
||||
if fault == "angle":
|
||||
mutated[3] ^= 1
|
||||
elif fault == "curvature":
|
||||
mutated[1] ^= 1
|
||||
elif fault == "checksum":
|
||||
mutated[7] ^= 16
|
||||
elif fault == "version":
|
||||
mutated[7] ^= 1
|
||||
elif fault == "counter":
|
||||
mutated[4] ^= 4
|
||||
if fault in ("angle", "curvature", "counter"):
|
||||
mutated[7] = 1 | (checksum4(mutated) << 4)
|
||||
bad = good if fault == "replay" else (addr, bytes(mutated), bus)
|
||||
self.send(bad, False)
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.send(lateral(packer, 0, command, canfd=bool(word & 2), counter=1), False)
|
||||
# A new valid announcement restores the same bounded path without relaxing native checks.
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
self.send(next_good)
|
||||
self.send(lateral(packer, 0, command, canfd=bool(word & 2), counter=1))
|
||||
|
||||
def test_actual_core_packer_boundary_all_namespaces(self):
|
||||
words = [128, 129, 130]
|
||||
if self.debug:
|
||||
words.append(131)
|
||||
for word in words:
|
||||
for direction in (-1, 1):
|
||||
self.reset_word(word)
|
||||
core = AngleController(low_gain=0.95, high_gain=0.95)
|
||||
active_seen = False
|
||||
for _ in range(40):
|
||||
command = core.step(Inputs(True, 15.0, 0.0, direction * 0.02))
|
||||
active_seen |= command.active
|
||||
self.tick(command)
|
||||
self.assertTrue(active_seen)
|
||||
self.tick(Command())
|
||||
|
||||
def test_cold_and_stale_shadow_require_new_valid_announcement(self):
|
||||
command = Command(True, 0.005, 0.005 / 15.0)
|
||||
self.warm()
|
||||
self.send(self.control(command), False)
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
self.now += 60_001
|
||||
self.warm()
|
||||
self.send(self.control(command), False)
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
|
||||
def test_invalid_shadow_version_checksum_and_dbc_bits_withdraw(self):
|
||||
command = Command(True, 0.005, 0.005 / 15.0)
|
||||
for index, mask in ((7, 1), (7, 16), (0, 32), (4, 2), (4, 32)):
|
||||
self.reset_word(128)
|
||||
valid = self.shadow(command)
|
||||
addr, data, bus = valid
|
||||
mutated = bytearray(data)
|
||||
mutated[index] ^= mask
|
||||
if index != 7:
|
||||
mutated[7] = 1 | (checksum4(mutated) << 4)
|
||||
self.send((addr, bytes(mutated), bus), False)
|
||||
self.send(self.control(command), False)
|
||||
self.send(valid)
|
||||
self.send(self.control(command))
|
||||
|
||||
def test_replay_wrong_bus_and_length_cannot_refresh_shadow(self):
|
||||
command = Command(True, 0.005, 0.005 / 15.0)
|
||||
self.warm()
|
||||
msg = self.shadow(command)
|
||||
self.send(msg)
|
||||
self.send(msg, False)
|
||||
self.send(self.control(command), False)
|
||||
for bus, size in ((2, 8), (0, 7), (0, 16)):
|
||||
addr, dat, _ = self.shadow(command)
|
||||
self.send((addr, (dat + b"\x00" * 8)[:size], bus), False)
|
||||
self.now += 30_000
|
||||
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 0})
|
||||
self.send(self.shadow(Command()))
|
||||
self.now += 30_000
|
||||
self.warm()
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
|
||||
def test_native_actual_path_cannot_be_laundered_by_shadow(self):
|
||||
for command in (Command(True, 0.0005, 0.0), Command(True, -0.0005, 0.0), Command(True, 0.5, 0.0), Command(True, -0.01, 0.001),
|
||||
Command(True, 0.04, 0.002), Command(True, 0.10, 0.005)):
|
||||
self.reset_word(128)
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command), False)
|
||||
# A rejected command does not seed an excessive subsequent ROC.
|
||||
self.reset_word(128)
|
||||
large = Command(True, 0.20, 0.01)
|
||||
self.send(self.shadow(large))
|
||||
self.send(self.control(large), False)
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
small = Command(True, 0.005, 0.005 / 15.0)
|
||||
self.send(self.shadow(small))
|
||||
self.send(self.control(small))
|
||||
|
||||
def test_permission_and_rx_withdrawals(self):
|
||||
command = Command(True, 0.005, 0.005 / 15.0)
|
||||
for source in ("brake", "gas", "cruise", "speed_quality", "yaw_checksum", "stale"):
|
||||
self.reset_word(128)
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
self.send(self.shadow(command))
|
||||
if source == "brake":
|
||||
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 2, "CcStat_D_Actl": 5})
|
||||
elif source == "gas":
|
||||
self.rx("EngVehicleSpThrottle", {"ApedPos_Pc_ActlArb": 50})
|
||||
elif source == "cruise":
|
||||
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 3})
|
||||
elif source == "speed_quality":
|
||||
self.assertFalse(self._rx(self.packer.make_can_msg_safety("BrakeSysFeatures", 0, {"VehVActlBrk_D_Qf": 0})))
|
||||
elif source == "yaw_checksum":
|
||||
packet = self.packer.make_can_msg_safety("Yaw_Data_FD1", 0, {"VehYawWActl_D_Qf": 3})
|
||||
packet[0].data[4] ^= 1
|
||||
self.assertFalse(self._rx(packet))
|
||||
else:
|
||||
self.now += 200_001
|
||||
self.safety.set_timer(self.now)
|
||||
self.send(self.control(command), False)
|
||||
self.send(self.control(Command()))
|
||||
|
||||
def test_fd_bad_crc_counter_and_mode_neutral_recovery(self):
|
||||
self.reset_word(130)
|
||||
command = Command(True, 0.005, 0.005 / 15.0)
|
||||
self.send(self.shadow(command))
|
||||
good = self.control(command)
|
||||
addr, dat, bus = good
|
||||
bad = bytearray(dat)
|
||||
bad[1] ^= 1
|
||||
self.send((addr, bytes(bad), bus), False)
|
||||
self.send(good, False) # bad CRC withdrew shadow
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(Command())) # valid mode0 resynchronizes skipped counters
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
self.send(self.shadow(command))
|
||||
self.control_count = (self.control_count + 1) % 16
|
||||
self.send(self.control(command), False)
|
||||
|
||||
def test_adjacent_profile_version_and_release_long_denials(self):
|
||||
command = Command(True, 0.005, 0.005 / 15.0)
|
||||
for word in (0, 2, 4, 8, 10, 12, 18, 32, 66, 132, 134, 136, 160, 194, 256):
|
||||
self.reset_word(word)
|
||||
announcement = self.shadow(command)
|
||||
if word & 128:
|
||||
self.send(announcement, False)
|
||||
else:
|
||||
self._tx(self.packet(announcement)) # Existing neutral metadata is not angle authority.
|
||||
self.send(self.control(command), False)
|
||||
self.reset_word(131)
|
||||
expected = bool(self.debug)
|
||||
self.send(self.shadow(command), expected)
|
||||
self.send(self.control(command), expected)
|
||||
|
||||
def test_rejected_metadata_does_not_erase_last_accepted_path_and_neutral_resync(self):
|
||||
self.reset_word(128)
|
||||
# At25m/s ROC is tight enough that erasing the accepted path would reject a same-path retry.
|
||||
core = AngleController(low_gain=.95, high_gain=.95)
|
||||
for _ in range(30):
|
||||
self.now += 50_000
|
||||
self.warm(speed=25., curvature=.001)
|
||||
command = core.step(Inputs(True, 25., -.001, -.001))
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
self.assertGreater(abs(command.path_angle), .01)
|
||||
self.now += 50_000
|
||||
self.warm(speed=25., curvature=.001)
|
||||
good = self.shadow(command)
|
||||
addr, data, bus = good
|
||||
bad = bytearray(data)
|
||||
bad[7] ^= 16
|
||||
self.send((addr, bytes(bad), bus), False)
|
||||
self.send(good)
|
||||
self.send(self.control(command))
|
||||
self.now += 50_000
|
||||
self.warm(speed=25., curvature=.001)
|
||||
self.send(self.control(Command()))
|
||||
self.now += 50_000
|
||||
self.warm(speed=25., curvature=.001)
|
||||
recovery = AngleController(low_gain=.95, high_gain=.95).step(Inputs(True, 25., -.001, -.001))
|
||||
self.send(self.shadow(recovery))
|
||||
self.send(self.control(recovery))
|
||||
|
||||
def test_rt_rate_denial_expires_and_neutral_can_recover(self):
|
||||
self.reset_word(130)
|
||||
for _ in range(8):
|
||||
self.send(self.control(Command()))
|
||||
self.send(self.control(Command()), False)
|
||||
self.now += 250_000
|
||||
self.warm()
|
||||
self.send(self.control(Command()))
|
||||
self.now += 50_000
|
||||
self.warm()
|
||||
command = Command(True, .005, .005 / 15.)
|
||||
self.send(self.shadow(command))
|
||||
self.send(self.control(command))
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2018, Comma.ai, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Custom MIT License
|
||||
|
||||
Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
|
||||
|
||||
1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
|
||||
|
||||
2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
|
||||
"This software is licensed under a custom license requiring permission for use."
|
||||
|
||||
3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
|
||||
"This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
|
||||
|
||||
4. **No Warranty**: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
|
||||
|
||||
---
|
||||
|
||||
Haibin Wen, SUNNYPILOT LLC
|
||||
@@ -204,8 +204,8 @@ class CANParser:
|
||||
strings = [strings]
|
||||
|
||||
for addr in self.addresses:
|
||||
for k in self.vl_all[addr]:
|
||||
self.vl_all[addr][k].clear()
|
||||
for values in self.vl_all[addr].values():
|
||||
values.clear()
|
||||
|
||||
updated_addrs: set[int] = set()
|
||||
for entry in strings:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.can.tests import TEST_DBC
|
||||
|
||||
|
||||
class TestParserClearValues(unittest.TestCase):
|
||||
def test_empty_update_clears_every_list_without_replacing_aliases(self):
|
||||
parser = CANParser(TEST_DBC, [("CAN_FD_MESSAGE", 10)], 0)
|
||||
packer = CANPacker(TEST_DBC)
|
||||
first = packer.make_can_msg("CAN_FD_MESSAGE", 0, {"SIGNED": 12})
|
||||
second = packer.make_can_msg("CAN_FD_MESSAGE", 0, {"SIGNED": 34})
|
||||
parser.update([(1_000_000, [first]), (2_000_000, [second])])
|
||||
values = parser.vl_all["CAN_FD_MESSAGE"]
|
||||
self.assertIs(values, parser.vl_all[first[0]])
|
||||
self.assertEqual(values["SIGNED"], [12, 34])
|
||||
# Extra caller-created entries are also part of the public mapping.
|
||||
values["extra"] = [56]
|
||||
aliases = dict(values)
|
||||
timestamps = dict(parser.ts_nanos["CAN_FD_MESSAGE"])
|
||||
latest = dict(parser.vl["CAN_FD_MESSAGE"])
|
||||
|
||||
self.assertEqual(parser.update([]), set())
|
||||
self.assertEqual(set(values), set(aliases))
|
||||
for key, value in aliases.items():
|
||||
self.assertIs(values[key], value)
|
||||
self.assertEqual(value, [])
|
||||
self.assertEqual(parser.ts_nanos["CAN_FD_MESSAGE"], timestamps)
|
||||
self.assertEqual(parser.vl["CAN_FD_MESSAGE"], latest)
|
||||
self.assertEqual(parser._last_update_nanos, 2_000_000)
|
||||
self.assertEqual(parser.last_nonempty_nanos, 2_000_000)
|
||||
|
||||
def test_lazily_added_message_and_repeated_empty_updates(self):
|
||||
parser = CANParser(TEST_DBC, [], 0)
|
||||
parser.vl["Brake_Status"]
|
||||
values = parser.vl_all["Brake_Status"]
|
||||
values["Signal1"] = [1, 2]
|
||||
alias = values["Signal1"]
|
||||
for _ in range(3):
|
||||
self.assertEqual(parser.update([]), set())
|
||||
self.assertIs(values["Signal1"], alias)
|
||||
self.assertEqual(alias, [])
|
||||
alias.append(3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,6 +1,8 @@
|
||||
import math
|
||||
import numpy as np
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.bluepilot_lateral import BluePilotLateral
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, apply_hysteresis, structs
|
||||
from opendbc.car.ford import fordcan
|
||||
from opendbc.car.ford.manual_turn import ManualTurnLatch
|
||||
@@ -15,6 +17,7 @@ from opendbc.car.ford.generic_canfd_lateral import (
|
||||
GenericCanfdLateralController, qualified as generic_canfd_qualified, bounded_command as generic_canfd_bounded_command,
|
||||
)
|
||||
from opendbc.car.ford.mache_lateral import MachELateralController, FordLateralResult, bounded_command, qualified as mache_qualified
|
||||
from opendbc.car.ford.new_port_curvature import create_controller as create_new_port_curvature, bounded_command as new_port_bounded_command
|
||||
from opendbc.car.ford.values import CarControllerParams, FordFlags, FordSafetyFlags, CAR
|
||||
from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
|
||||
|
||||
@@ -66,6 +69,7 @@ class CarController(CarControllerBase):
|
||||
self.packer = CANPacker(dbc_names[Bus.pt])
|
||||
self.CAN = fordcan.CanBus(CP)
|
||||
|
||||
self.bp_lat = BluePilotLateral(CP) if angle_qualified(CP) else None
|
||||
self.manual_turn = ManualTurnLatch() if CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not CP.flags & FordFlags.LKA_STEERING else None
|
||||
self.stock_cruise_button = FordStockCruiseButton()
|
||||
self.manual_turn_inputs = None
|
||||
@@ -73,6 +77,7 @@ class CarController(CarControllerBase):
|
||||
self.mache_extended_announced = False
|
||||
self.generic_canfd = generic_canfd_qualified(CP)
|
||||
self.classic_lateral = GenericCanfdLateralController(CP) if self.generic_canfd else create_classic_controller(CP)
|
||||
self.new_port_curvature = create_new_port_curvature(CP)
|
||||
self.classic_extended_announced = False
|
||||
self.classic_profile = any(s.safetyModel == structs.CarParams.SafetyModel.ford and
|
||||
s.safetyParam & (FordSafetyFlags.CLASSIC_EXTENDED | FordSafetyFlags.GENERIC_CANFD_EXTENDED) for s in CP.safetyConfigs)
|
||||
@@ -93,7 +98,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
def update(self, CC, CS, now_nanos):
|
||||
can_sends = []
|
||||
if (self.mache_lateral is not None or self.classic_lateral is not None) and self.manual_turn_inputs is not None:
|
||||
if (self.mache_lateral is not None or self.classic_lateral is not None or self.new_port_curvature is not None) and self.manual_turn_inputs is not None:
|
||||
self.manual_turn_inputs.update()
|
||||
self.manual_turn_inputs.apply_blend_settings(self)
|
||||
|
||||
@@ -130,7 +135,10 @@ class CarController(CarControllerBase):
|
||||
|
||||
### lateral control ###
|
||||
# send steer msg at 20Hz
|
||||
if (self.frame % CarControllerParams.STEER_STEP) == 0 and self.CP.flags & FordFlags.LKA_STEERING:
|
||||
if (self.frame % CarControllerParams.STEER_STEP) == 0 and self.bp_lat is not None:
|
||||
can_sends += self.bp_lat.step_lateral(CC, CS, self.packer, self.CAN, self.frame, now_nanos)
|
||||
self.apply_curvature_last = self.bp_lat.apply_curvature_last
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.CP.flags & FordFlags.LKA_STEERING:
|
||||
# Preserve the stock LMC heartbeat without requesting lateral action.
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, False, 0., 0., 0., 0.,
|
||||
stock_lmc=CS.lateral_motion_control))
|
||||
@@ -175,6 +183,22 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(create_extended_classic_lat_ctl_msg(
|
||||
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
|
||||
-lateral.curvature, -lateral.curvature_rate))
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.new_port_curvature is not None:
|
||||
inputs = self.manual_turn_inputs.lateral_snapshot(CS.out.vEgoRaw) if self.manual_turn_inputs is not None else None
|
||||
self.new_port_curvature.set_inputs(*(inputs if inputs is not None else
|
||||
(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)))
|
||||
previous = self.new_port_curvature.curvature_last
|
||||
demanded = self.new_port_curvature.update(CC, CS, actuators)
|
||||
lateral = new_port_bounded_command(self.new_port_curvature, demanded, previous, CS.out.vEgoRaw,
|
||||
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
|
||||
self.new_port_curvature_demand = demanded
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, int(lateral.active), 0., 0.,
|
||||
-lateral.curvature, 0., counter))
|
||||
else:
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, lateral.active, 0., 0., -lateral.curvature, 0.))
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0:
|
||||
lateral_active = CC.latActive and not self.mache_profile and not self.classic_profile
|
||||
manual_turn = False
|
||||
@@ -222,7 +246,9 @@ class CarController(CarControllerBase):
|
||||
|
||||
# send lka msg at 33Hz
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0:
|
||||
if self.CP.flags & FordFlags.LKA_STEERING:
|
||||
if self.bp_lat is not None:
|
||||
can_sends += self.bp_lat.step_lka(self.packer, self.CAN, now_nanos)
|
||||
elif self.CP.flags & FordFlags.LKA_STEERING:
|
||||
source_age_ns = now_nanos - CS.lkas_available_ts_nanos
|
||||
lka_active = (CC.latActive and CS.lkas_available and 0 <= source_age_ns <= 100_000_000 and
|
||||
(CS.out.cruiseState.enabled or qualified_aol(self.CP, marked_only=True)) and
|
||||
|
||||
@@ -6,6 +6,7 @@ THIRD_PARTY_NOTICES.md.
|
||||
"""
|
||||
|
||||
from dataclasses import replace
|
||||
from typing import Protocol
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -23,7 +24,7 @@ class ExplorerLateralController(FordLateralController):
|
||||
super().__init__(CP)
|
||||
self.manual_turn_detected = False
|
||||
|
||||
def _manual_turn(self, CC, CS, desired, driver_assisting) -> bool:
|
||||
def _manual_turn(self, CC, CS, desired: float, driver_assisting: bool = False) -> bool:
|
||||
self.manual_turn_detected = super()._manual_turn(CC, CS, desired, driver_assisting)
|
||||
return self.manual_turn_detected
|
||||
|
||||
@@ -47,7 +48,13 @@ def qualified(CP) -> bool:
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
|
||||
|
||||
|
||||
def bounded_command(owner: ExplorerLateralController, demanded: FordLateralResult,
|
||||
class CurvatureOwner(Protocol):
|
||||
manual_turn_detected: bool
|
||||
curvature_last: float
|
||||
path_angle_last: float
|
||||
|
||||
|
||||
def bounded_command(owner: CurvatureOwner, demanded: FordLateralResult,
|
||||
previous: float, speed: float, measured: float, *, absolute_cap: float | None = None) -> FordLateralResult:
|
||||
"""Keep classic demand inside the modern common ISO/jerk envelope."""
|
||||
limits = CarControllerParams.CURVATURE_LIMITS
|
||||
|
||||
@@ -35,7 +35,7 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
ret.radarUnavailable = Bus.radar not in DBC[candidate]
|
||||
ret.steerControlType = structs.CarParams.SteerControlType.angle
|
||||
ret.steerActuatorDelay = 0.05 if ret.flags & FordFlags.LKA_STEERING else 0.2
|
||||
ret.steerActuatorDelay = 0.05 if ret.flags & FordFlags.LKA_STEERING else 0.22
|
||||
ret.steerLimitTimer = 1.0
|
||||
ret.steerAtStandstill = True
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Reuse the current Ford curvature strategy for exact Edge/Mondeo callers.
|
||||
|
||||
Only scalar curvature crosses the existing native boundary; no protocol extension.
|
||||
The reused strategy retains its required provenance and notices in lateral_strategy.py.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
import math
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
|
||||
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
|
||||
PROFILES = {
|
||||
CAR.FORD_EDGE_MK2: (int(FordFlags.NEW_PORT | FordFlags.ALT_STEER_ANGLE), 8),
|
||||
CAR.FORD_MONDEO_MK5: (int(FordFlags.NEW_PORT | FordFlags.CANFD), 10),
|
||||
}
|
||||
|
||||
|
||||
def qualified(cp):
|
||||
profile = PROFILES.get(cp.carFingerprint)
|
||||
if (profile is None or cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or
|
||||
cp.alternativeExperience not in (0, 32) or not cp.pcmCruise or
|
||||
cp.steerControlType != structs.CarParams.SteerControlType.angle or
|
||||
len(cp.safetyConfigs) not in (1, 2) or (cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1)):
|
||||
return False
|
||||
flags, word = profile
|
||||
if (int(cp.flags) & ~int(FordFlags.HAS_BSM)) != flags:
|
||||
return False
|
||||
if len(cp.safetyConfigs) == 2 and (cp.safetyConfigs[0].safetyModel != structs.CarParams.SafetyModel.noOutput or
|
||||
cp.safetyConfigs[0].safetyParam != 0):
|
||||
return False
|
||||
safety = cp.safetyConfigs[-1]
|
||||
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == word + int(cp.openpilotLongitudinalControl)
|
||||
|
||||
|
||||
class NewPortCurvatureController(FordLateralController):
|
||||
def __init__(self, cp):
|
||||
if not qualified(cp):
|
||||
raise ValueError("Edge/Mondeo curvature requires its exact current native profile")
|
||||
super().__init__(cp)
|
||||
self.manual_turn_detected = False
|
||||
|
||||
def _manual_turn(self, CC, CS, desired: float, driver_assisting: bool = False) -> bool:
|
||||
self.manual_turn_detected = super()._manual_turn(CC, CS, desired, driver_assisting)
|
||||
return self.manual_turn_detected
|
||||
|
||||
def update(self, CC, CS, actuators):
|
||||
out = CS.out
|
||||
if (not CC.latActive or not out.canValid or out.canTimeout or out.steerFaultTemporary or
|
||||
out.steerFaultPermanent or out.vehicleSensorsInvalid or
|
||||
not all(math.isfinite(value) for value in (out.vEgoRaw, out.yawRate, out.steeringAngleDeg, actuators.curvature))):
|
||||
self.human_turn.reset()
|
||||
self.curvature_samples.clear()
|
||||
self.curvature_last = self.path_angle_last = 0.0
|
||||
self.manual_turn_detected = False
|
||||
return FordLateralResult()
|
||||
return super().update(CC, CS, actuators)
|
||||
|
||||
|
||||
def create_controller(cp):
|
||||
return NewPortCurvatureController(cp) if qualified(cp) else None
|
||||
|
||||
|
||||
def bounded_command(owner, demanded, previous, speed, measured):
|
||||
cap = (3.0 - 9.81 * .06) / max(speed, 1.) ** 2 if owner.CP.flags & FordFlags.CANFD else None
|
||||
command = shared_bound(owner, demanded, previous, speed, measured, absolute_cap=cap)
|
||||
# These profiles keep their current curvature-only native/codec contract.
|
||||
return replace(command, curvature_rate=0.0, path_angle=0.0, ramp_type=0, precision_type=1)
|
||||
@@ -27,6 +27,9 @@ class FordStockCruiseButton:
|
||||
|
||||
|
||||
def qualified(CP) -> bool:
|
||||
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
|
||||
if angle_qualified(CP):
|
||||
return not CP.openpilotLongitudinalControl
|
||||
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
|
||||
return False
|
||||
"""Existing stock profiles with the paired physical-driver button contract."""
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Current-strategy Edge/Mondeo wiring and current native scalar-curvature proof.
|
||||
|
||||
Source-only authored cases; runtime must bind actual DEBUG/RELEASE libraries.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from openpilot.starpilot.tests import test_vehicle_preferences as startup
|
||||
from openpilot.starpilot.lateral.tests import test_lane_runtime as lane
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.ford.new_port_curvature import qualified, create_controller
|
||||
from opendbc.car.ford.tests.test_three_ports import params as factory
|
||||
from opendbc.car.ford.values import CAR, FordFlags
|
||||
from opendbc.safety.tests import test_ford_aol as physical
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
PROFILES = (CAR.FORD_EDGE_MK2, CAR.FORD_MONDEO_MK5)
|
||||
|
||||
|
||||
class TestNewPortCurvature(unittest.TestCase):
|
||||
def test_exact_existing_factory_profiles_and_no_namespace_or_topology_inference(self):
|
||||
for identity in PROFILES:
|
||||
for alpha, release in ((False, False), (True, False), (False, True), (True, True)):
|
||||
cp = factory(identity, alpha, release)
|
||||
expected_long = bool(alpha and (identity == CAR.FORD_EDGE_MK2 or not release))
|
||||
self.assertTrue(cp.pcmCruise)
|
||||
self.assertEqual(cp.openpilotLongitudinalControl, expected_long)
|
||||
self.assertEqual(cp.safetyConfigs[-1].safetyParam, (8 if identity == CAR.FORD_EDGE_MK2 else 10) + int(expected_long))
|
||||
self.assertTrue(qualified(cp))
|
||||
self.assertIsNotNone(create_controller(cp))
|
||||
original = cp.to_bytes()
|
||||
cp.clear_write_flag()
|
||||
create_controller(cp)
|
||||
self.assertEqual(cp.to_bytes(), original)
|
||||
cp.clear_write_flag()
|
||||
for field, value in (("passive", True), ("dashcamOnly", True), ("notCar", True), ("alternativeExperience", 64),
|
||||
("pcmCruise", False), ("openpilotLongitudinalControl", not expected_long)):
|
||||
with structs.CarParams.from_bytes(original) as prior:
|
||||
changed = prior.as_builder()
|
||||
setattr(changed, field, value)
|
||||
self.assertFalse(qualified(changed))
|
||||
for word in (0, 2, 12, 18, 32, 66, 128, 130, 65535):
|
||||
with structs.CarParams.from_bytes(original) as prior:
|
||||
changed = prior.as_builder()
|
||||
changed.safetyConfigs[-1].safetyParam = word
|
||||
self.assertIsNone(create_controller(changed))
|
||||
cp.alternativeExperience = 32
|
||||
self.assertTrue(qualified(cp))
|
||||
cp.safetyConfigs = [cp.safetyConfigs[-1], cp.safetyConfigs[-1]]
|
||||
self.assertFalse(qualified(cp))
|
||||
self.assertIsNone(create_controller(factory(CAR.FORD_TRANSIT_MK5)))
|
||||
self.assertIsNone(create_controller(factory(CAR.FORD_BRONCO_SPORT_MK1)))
|
||||
|
||||
def test_saved_card_controls_actual_parser_provider_sender_and_native_both_signs(self):
|
||||
safety = libsafety_py.libsafety
|
||||
for identity in PROFILES:
|
||||
for direction in (-1, 1):
|
||||
with self.subTest(identity=identity, direction=direction), OpenpilotPrefix(), \
|
||||
patch.dict(os.environ, {"REPLAY": "1", "SIMULATION": "1"}), \
|
||||
patch("openpilot.selfdrive.controls.controlsd.messaging.PubMaster"):
|
||||
saved = Params()
|
||||
helper = startup.TestVehicleStartupPreferences()
|
||||
helper.setUp()
|
||||
helper.params = saved
|
||||
native = physical.TestFordAolDriverIntent()
|
||||
try:
|
||||
for key, value in (("OpenpilotEnabledToggle", True), ("SafeMode", False), ("AlwaysOnLateral", False),
|
||||
("AlphaLongitudinalEnabled", False), ("IsReleaseBranch", False)):
|
||||
saved.put_bool(key, value, block=True)
|
||||
observed = gen_empty_fingerprint()
|
||||
observed[0][0x5A] = 8
|
||||
observed[2].update({0x3D6: 8, 0x186: 8})
|
||||
host, constructed, cp = helper.start(identity, key="FordHumanTurnDetection", observed=observed,
|
||||
capture=lambda ci: (ci.CP.safetyConfigs[-1].safetyParam, ci.CC.new_port_curvature is not None))
|
||||
self.assertEqual(constructed, [(8 if identity == CAR.FORD_EDGE_MK2 else 10, True)])
|
||||
self.assertEqual(host.CI.CP.to_dict(), cp.to_dict())
|
||||
self.assertTrue(qualified(cp))
|
||||
owner = host.CI.CC.new_port_curvature
|
||||
inputs = host.CI.CC.manual_turn_inputs
|
||||
self.assertIsNotNone(inputs)
|
||||
inputs.sm = messaging.SubMaster(["modelV2", "lateralDelay"])
|
||||
host.sm = messaging.SubMaster(["carControl"])
|
||||
controls = Controls()
|
||||
native.setUp()
|
||||
native.word, native.tick = cp.safetyConfigs[-1].safetyParam, 0
|
||||
safety.set_alternative_experience(0)
|
||||
self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, native.word), 0)
|
||||
safety.init_tests()
|
||||
received, sent, active_curvatures = [], [], []
|
||||
|
||||
def rx(name, values, *, native=native, received=received):
|
||||
self.assertLessEqual(set(values), set(native.packer.dbc.name_to_msg[name].sigs))
|
||||
frame = native.packer.make_can_msg(name, 0, values)
|
||||
addr, data, bus = frame
|
||||
self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data)))
|
||||
received.append(frame)
|
||||
|
||||
host.publish_sendcan = lambda frames, valid=True, sent=sent: sent.extend(frames) if valid else self.fail("Invalid Card trace")
|
||||
host.ci_initialized = True
|
||||
host.CI.update([])
|
||||
saw_human_zero = saw_fault_zero = saw_stale_model = saw_rate_neutral = False
|
||||
for tick in range(600):
|
||||
received.clear()
|
||||
with patch.object(native, "rx", rx):
|
||||
native.pump(1, main=0 if tick < 30 else 4, speed=15.)
|
||||
now = (1_000_000 + native.tick * 10_000) * 1000
|
||||
pressed = 100 <= tick < 430
|
||||
fault = 460 <= tick < 480
|
||||
extra: list[tuple[str, int, dict[str, float]]] = [("Cluster_Info1_FD1", 0, {"AccEnbl_B_RqDrv": 1}),
|
||||
("BodyInfo_3_FD1", 0, {}), ("RCMStatusMessage2_FD1", 0, {"FirstRowBuckleDriver": 1}),
|
||||
("INSTRUMENT_PANEL", 0, {}), ("IPMA_Data", 2, {}), ("ACCDATA", 2, {}),
|
||||
("ACCDATA_2", 2, {}), ("ACCDATA_3", 2, {}),
|
||||
("EPAS_INFO", 0, {"SteeringColumnTorque": 2 if pressed else 0, "EPAS_Failure": 2 if fault else 0})]
|
||||
if cp.flags & FordFlags.ALT_STEER_ANGLE:
|
||||
extra += [("ParkAid_Data", 0, {"ExtSteeringAngleReq2": 60 if pressed else 0, "EPASExtAngleStatReq": 0, "ApaSys_D_Stat": 0}),
|
||||
("SteeringPinion_Data_Alt", 0, {"StePinRelInit_An_Sns": 60 if pressed else 0})]
|
||||
else:
|
||||
extra += [("SteeringPinion_Data", 0, {"StePinComp_An_Est": 60 if pressed else 0, "StePinCompAnEst_D_Qf": 3})]
|
||||
host_sources = [native.packer.make_can_msg(name, bus, values) for name, bus, values in extra]
|
||||
for addr, data, bus in host_sources:
|
||||
self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data)))
|
||||
received.extend(host_sources)
|
||||
state = host.CI.update([(now, list(received))])
|
||||
if tick < 40:
|
||||
continue
|
||||
self.assertTrue(state.canValid)
|
||||
self.assertFalse(state.canTimeout)
|
||||
self.assertTrue(safety.get_controls_allowed())
|
||||
with patch("openpilot.starpilot.controller_extensions.time.monotonic_ns", return_value=now), \
|
||||
patch("openpilot.selfdrive.car.card.time.monotonic", return_value=now / 1e9), \
|
||||
patch.object(inputs.sm, "update", return_value=None):
|
||||
feed_events = []
|
||||
with patch.object(controls.sm, "update_msgs", side_effect=lambda _, events, collector=feed_events: collector.extend(events)):
|
||||
lane.feed(controls, now, tick, speed=15.)
|
||||
car_state = messaging.new_message("carState", valid=True, logMonoTime=now)
|
||||
car_state.carState = state
|
||||
|
||||
preview = messaging.new_message("modelV2", valid=True, logMonoTime=now)
|
||||
preview.modelV2 = lane.model()
|
||||
preview.modelV2.action.desiredCurvature = direction * .0002
|
||||
predicted = .012 + (tick - 430) * .0002 if 430 <= tick < 460 else .0012
|
||||
preview.modelV2.orientationRate.z = [direction * predicted * 15.] * 33
|
||||
delay = messaging.new_message("lateralDelay", valid=True, logMonoTime=now)
|
||||
delay.lateralDelay.lateralDelay = .38
|
||||
replacements = {"carState": car_state.as_reader(), "modelV2": preview.as_reader(), "lateralDelay": delay.as_reader()}
|
||||
controls.sm.update_msgs(now / 1e9, [event for event in feed_events
|
||||
if event.which() not in replacements] + list(replacements.values()))
|
||||
if tick < 490:
|
||||
inputs.sm.update_msgs(now / 1e9, [preview.as_reader(), delay.as_reader()])
|
||||
command, controller_log = controls.state_control()
|
||||
self.assertTrue(command.enabled)
|
||||
control_events = []
|
||||
def capture_control(service, event, collector=control_events):
|
||||
if service == "carControl":
|
||||
collector.append(messaging.log_from_bytes(event.to_bytes()))
|
||||
with patch.object(controls.pm, "send", side_effect=capture_control):
|
||||
controls.publish(command, controller_log)
|
||||
self.assertEqual(len(control_events), 1)
|
||||
self.assertEqual(control_events[0].carControl.to_dict(), command.to_dict())
|
||||
self.assertEqual(control_events[0].valid, state.canValid)
|
||||
host.sm.update_msgs(now / 1e9, control_events)
|
||||
command = host.sm["carControl"]
|
||||
host.can_log_mono_time = now
|
||||
sent.clear()
|
||||
host.controls_update(state, command)
|
||||
if tick > 510:
|
||||
self.assertIsNone(owner.model)
|
||||
saw_stale_model = True
|
||||
for addr, data, bus in sent:
|
||||
if addr not in (0x3CA, 0x3D3, 0x3D6):
|
||||
continue
|
||||
self.assertTrue(safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, data)), (identity, tick, hex(addr)))
|
||||
if addr == 0x3CA:
|
||||
self.assertEqual(data[4] & 3, 0) # Existing neutral body, no extension announcement.
|
||||
elif addr in (0x3D3, 0x3D6):
|
||||
fd = addr == 0x3D6
|
||||
mode = (data[0] >> 4) & 7 if fd else (data[4] >> 2) & 7
|
||||
curvature = ((data[2] << 3) | (data[3] >> 5)) - 1000 if fd else ((data[0] << 3) | (data[1] >> 5)) - 1000
|
||||
rate = (data[6] << 3) | (data[7] >> 5) if fd else ((data[1] & 31) << 8) | data[2]
|
||||
self.assertEqual(rate, 1024 if fd else 4096)
|
||||
if 430 <= tick < 460 and host.CI.CC.new_port_curvature_demand.curvature_rate != 0:
|
||||
saw_rate_neutral = True
|
||||
if 400 <= tick < 425:
|
||||
self.assertEqual((mode, curvature), (0, 0))
|
||||
saw_human_zero = True
|
||||
if fault:
|
||||
self.assertEqual((mode, curvature), (0, 0))
|
||||
saw_fault_zero = True
|
||||
if 70 <= tick < 100 and mode and curvature:
|
||||
active_curvatures.append(curvature)
|
||||
self.assertAlmostEqual(owner._curvature_lookahead(), .38, places=6)
|
||||
self.assertGreater(direction * host.CI.CC.new_port_curvature_demand.curvature, .0002)
|
||||
self.assertTrue(active_curvatures)
|
||||
self.assertTrue(all(value * direction < 0 for value in active_curvatures))
|
||||
self.assertTrue(saw_human_zero and saw_fault_zero and saw_stale_model and saw_rate_neutral)
|
||||
self.assertEqual(Path(saved.get_param_path("FordHumanTurnDetection")).read_bytes(), b"1")
|
||||
finally:
|
||||
helper.doCleanups()
|
||||
safety.set_alternative_experience(0)
|
||||
safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
|
||||
@@ -55,6 +55,7 @@ class TestFordThreePorts(unittest.TestCase):
|
||||
with self.subTest(candidate=candidate):
|
||||
stock = params(candidate)
|
||||
self.assertFalse(stock.openpilotLongitudinalControl)
|
||||
self.assertAlmostEqual(stock.steerActuatorDelay, 0.05 if candidate == CAR.FORD_TRANSIT_MK5 else 0.22, places=7)
|
||||
self.assertTrue(stock.safetyConfigs[-1].safetyParam & FordSafetyFlags.NEW_PORT)
|
||||
self.assertFalse(stock.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
|
||||
requested = params(candidate, alpha=True)
|
||||
@@ -62,14 +63,16 @@ class TestFordThreePorts(unittest.TestCase):
|
||||
self.assertTrue(requested.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
|
||||
if candidate == CAR.FORD_MONDEO_MK5:
|
||||
self.assertFalse(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
|
||||
self.assertAlmostEqual(params(CAR.FORD_MUSTANG_MACH_E_MK1).steerActuatorDelay, 0.22, places=7)
|
||||
self.assertFalse(params(CAR.FORD_BRONCO_SPORT_MK1).openpilotLongitudinalControl)
|
||||
self.assertTrue(params(CAR.FORD_BRONCO_SPORT_MK1, alpha=True).openpilotLongitudinalControl)
|
||||
|
||||
def test_real_edge_angle_and_mondeo_gear_parsers(self):
|
||||
for candidate, source_name, source_values in (
|
||||
cases: tuple[tuple[CAR, str, dict[str, float]], ...] = (
|
||||
(CAR.FORD_EDGE_MK2, "TransGearData", {"GearLvrPos_D_Actl": 4}),
|
||||
(CAR.FORD_MONDEO_MK5, "Gear_Shift_by_Wire_FD1", {"TrnRng_D_RqGsm": 4}),
|
||||
):
|
||||
)
|
||||
for candidate, source_name, source_values in cases:
|
||||
with self.subTest(candidate=candidate):
|
||||
cp = params(candidate)
|
||||
self.assertEqual(cp.transmissionType, structs.CarParams.TransmissionType.automatic)
|
||||
|
||||
@@ -48,6 +48,7 @@ class FordSafetyFlags(IntFlag):
|
||||
MACH_E_EXTENDED = 16
|
||||
CLASSIC_EXTENDED = 32
|
||||
EXPLORER_EXTENDED = CLASSIC_EXTENDED # Existing public profile spelling.
|
||||
BLUEPILOT_ANGLE = 128
|
||||
GENERIC_CANFD_EXTENDED = 64
|
||||
|
||||
|
||||
|
||||
@@ -568,7 +568,7 @@ class CarController(CarControllerBase):
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
stock_steer_ready = (stock_steer_ready and CS.pedal_sensor_healthy and
|
||||
0 < CS.pedal_sensor_ts_nanos <= now_nanos <= CS.pedal_sensor_ts_nanos + PEDAL_SENSOR_TIMEOUT_NS)
|
||||
lat_active = CC.latActive and stock_steer_ready
|
||||
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched
|
||||
|
||||
# Steering (Active: 50Hz, inactive: 10Hz)
|
||||
steer_step = self.params.STEER_STEP if lat_active else self.params.INACTIVE_STEER_STEP
|
||||
@@ -605,6 +605,7 @@ class CarController(CarControllerBase):
|
||||
self.apply_torque_last = apply_torque
|
||||
idx = self.lka_steering_cmd_counter % 4
|
||||
can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_torque, idx, lat_active))
|
||||
CS.steering_authority.command(now_nanos, lat_active)
|
||||
|
||||
status_bolt = self.CP.carFingerprint in NO_ACC_BOLT_CAR and (
|
||||
is_bolt_present_no_acc_pedal_profile(self.CP) or is_bolt_pedal_removed_profile(self.CP))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from opendbc.car.gm.steering_authority import SteeringAuthority
|
||||
from opendbc.car.gps import CarGpsTracker, get_car_gps_config
|
||||
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal, camera_acc_pedal_profile, BrakeSource, volt_cc_pedal_profile
|
||||
from opendbc.car.gm.values import is_volt_longitudinal, is_gm_auto_hold
|
||||
@@ -35,6 +36,7 @@ BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.D
|
||||
class CarState(CarStateBase):
|
||||
def __init__(self, CP):
|
||||
super().__init__(CP)
|
||||
self.steering_authority = SteeringAuthority(CP)
|
||||
self.car_gps_tracker = CarGpsTracker(CP)
|
||||
self.car_gps_supported = self.car_gps_tracker.config is not None
|
||||
self.hybrid_profile = malibu_hybrid_profile(CP)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from math import fabs, exp
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car import get_safety_config, structs
|
||||
from opendbc.car import Bus, get_safety_config, structs
|
||||
from opendbc.car.pedal import supported_pedal_detected
|
||||
from opendbc.car.gm.pedal_capability import pedal_candidate
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
@@ -50,6 +50,7 @@ class CarInterface(CarInterfaceBase):
|
||||
return super().apply(c, now_nanos)
|
||||
|
||||
def update(self, can_packets):
|
||||
self.CS.steering_authority.observe(can_packets)
|
||||
if malibu_hybrid_profile(self.CP) is not None:
|
||||
self.CS.hybrid_buttons.observe_packets(can_packets)
|
||||
if (is_bolt_pedal_removed_profile(self.CP) or is_bolt_pedal_removed_profile(self.CP, stock_only=True)):
|
||||
@@ -63,7 +64,9 @@ class CarInterface(CarInterfaceBase):
|
||||
if is_conventional_cc_pedal_profile(self.CP):
|
||||
self.CS.conventional_cancel_credit.observe(can_packets)
|
||||
if not is_bolt_cc_profile(self.CP):
|
||||
return super().update(can_packets)
|
||||
ret = super().update(can_packets)
|
||||
self.CS.steering_authority.update(ret, self.can_parsers[Bus.pt]._last_update_nanos)
|
||||
return ret
|
||||
adaptive = (self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL and
|
||||
self.CP.safetyConfigs[0].safetyParam == 0xC140)
|
||||
camera_source = getattr(self, "bolt_cc_camera_observed", (0, b""))
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.gm.values import GMFlags, camera_acc_pedal_profile, is_volt_ascm_longitudinal
|
||||
|
||||
# PSCMStatus is expected at 10 Hz. Three periods bound first acquisition and missing feedback.
|
||||
EPS_STATUS_TIMEOUT_NS = 300_000_000
|
||||
STEERING_COMMAND_TIMEOUT_NS = 100_000_000
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
def monitored_profile(CP: structs.CarParams) -> bool:
|
||||
return (is_volt_ascm_longitudinal(CP) and camera_acc_pedal_profile(CP) is None and
|
||||
not CP.flags & GMFlags.PEDAL_LONG)
|
||||
|
||||
|
||||
class SteeringAuthority:
|
||||
def __init__(self, CP: structs.CarParams):
|
||||
self.enabled = monitored_profile(CP)
|
||||
self.min_speed = CP.minSteerSpeed
|
||||
self.latched = False
|
||||
self.command_active = False
|
||||
self.neutral_since_ns = 0
|
||||
self.seen_active = False
|
||||
self.last_active_ns = 0
|
||||
self.active_since_ns = 0
|
||||
self.command_edge_ns = 0
|
||||
self.acquire_since_ns = 0
|
||||
self.status_ns = 0
|
||||
self.status = -1
|
||||
self.bad_since_ns = 0
|
||||
self.bad_samples = 0
|
||||
self.last_sample_ns = 0
|
||||
self.disable_ns = 0
|
||||
self.neutral_ns = 0
|
||||
self.main_off = False
|
||||
self.source_ns: dict[int, int] = {}
|
||||
|
||||
def observe(self, can_packets):
|
||||
if not self.enabled:
|
||||
return
|
||||
self.source_ns.clear()
|
||||
malformed = any(bus == 0 and address == 0x184 and (stamp <= 0 or len(data) != 8)
|
||||
for stamp, packets in can_packets for address, data, bus in packets)
|
||||
lengths = {0xC9: 8, 0xBE: 6, 0xBD: 7, 0x1E1: 7}
|
||||
invalid = {address for stamp, packets in can_packets for address, data, bus in packets
|
||||
if bus == 0 and address in lengths and (stamp <= 0 or len(data) != lengths[address])}
|
||||
for address in invalid:
|
||||
self.source_ns.pop(address, None)
|
||||
for stamp, packets in can_packets:
|
||||
for address, data, bus in packets:
|
||||
if bus != 0 or stamp <= 0:
|
||||
continue
|
||||
if address in lengths and address not in invalid:
|
||||
self.source_ns[address] = max(stamp, self.source_ns.get(address, 0))
|
||||
if address == 0x184 and stamp > self.status_ns:
|
||||
if self.active_since_ns and stamp - max(self.status_ns, self.active_since_ns) > EPS_STATUS_TIMEOUT_NS:
|
||||
self.latched = True
|
||||
self.status_ns = stamp
|
||||
self.status = -1 if malformed else (data[0] >> 3) & 7
|
||||
|
||||
def command(self, now_ns: int, active: bool):
|
||||
if not self.enabled:
|
||||
return
|
||||
if not active and self.command_active:
|
||||
self.neutral_since_ns = now_ns
|
||||
if active:
|
||||
if not self.command_active:
|
||||
self.command_edge_ns = now_ns
|
||||
# A measured inactive response to emitted neutral requires active acquisition.
|
||||
# Retain episode/feed history, and never renew an acquisition on brief edges.
|
||||
if (not self.acquire_since_ns and self.neutral_since_ns and
|
||||
self.neutral_since_ns < self.status_ns <= now_ns and
|
||||
now_ns - self.status_ns <= EPS_STATUS_TIMEOUT_NS and self.status == 0):
|
||||
self.acquire_since_ns = now_ns
|
||||
# A sustained emitted neutral with healthy inactive feedback is a new acquisition.
|
||||
# Brief withdrawals retain the confirmed episode; a fault latch never clears here.
|
||||
if (not self.latched and self.neutral_since_ns and
|
||||
now_ns - self.neutral_since_ns >= EPS_STATUS_TIMEOUT_NS and
|
||||
0 < self.status_ns <= now_ns and now_ns - self.status_ns <= EPS_STATUS_TIMEOUT_NS and
|
||||
self.status in (0, 1)):
|
||||
self.seen_active = False
|
||||
self.active_since_ns = 0
|
||||
self.acquire_since_ns = now_ns
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
self.last_sample_ns = self.status_ns
|
||||
self.neutral_since_ns = 0
|
||||
self.command_active = active
|
||||
if active:
|
||||
self.last_active_ns = now_ns
|
||||
if not self.active_since_ns:
|
||||
self.active_since_ns = now_ns
|
||||
elif self.disable_ns and now_ns > self.disable_ns:
|
||||
self.neutral_ns = now_ns
|
||||
|
||||
def update(self, CS: structs.CarState, now_ns: int):
|
||||
if not self.enabled:
|
||||
return
|
||||
healthy = CS.canValid and not CS.canTimeout
|
||||
|
||||
def fresh(address):
|
||||
stamp = self.source_ns.get(address, 0)
|
||||
return 0 < stamp <= now_ns and now_ns - stamp <= STEERING_COMMAND_TIMEOUT_NS
|
||||
|
||||
main_fresh = fresh(0xC9)
|
||||
buttons_fresh = fresh(0x1E1)
|
||||
pedal_fresh = (CS.brakePressed and (main_fresh or fresh(0xBE)) or
|
||||
CS.regenBraking and fresh(0xBD))
|
||||
cancel = buttons_fresh and any(b.type == ButtonType.cancel and b.pressed for b in CS.buttonEvents)
|
||||
if self.latched and healthy:
|
||||
if pedal_fresh or cancel or main_fresh and not CS.cruiseState.available:
|
||||
if not self.disable_ns:
|
||||
self.disable_ns = now_ns
|
||||
self.main_off |= main_fresh and not CS.cruiseState.available
|
||||
physical_enable = buttons_fresh and self.source_ns[0x1E1] > self.neutral_ns and CS.buttonEnable
|
||||
main_enable = self.main_off and main_fresh and self.source_ns[0xC9] > self.neutral_ns and CS.cruiseState.available
|
||||
if (self.neutral_ns > self.disable_ns > 0 and (physical_enable or main_enable) and
|
||||
CS.cruiseState.available and not CS.brakePressed and not CS.regenBraking):
|
||||
self.latched = False
|
||||
self.disable_ns = self.neutral_ns = 0
|
||||
self.main_off = False
|
||||
self.last_active_ns = self.active_since_ns = self.acquire_since_ns = 0
|
||||
self.seen_active = False
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
|
||||
deliberate_pause = (pedal_fresh or cancel or main_fresh and not CS.cruiseState.available or
|
||||
CS.standstill or CS.vEgo < self.min_speed or CS.steeringPressed or
|
||||
self.status in (2, 3) and 0 < self.status_ns <= now_ns and
|
||||
now_ns - self.status_ns <= EPS_STATUS_TIMEOUT_NS)
|
||||
if healthy and deliberate_pause and not self.latched:
|
||||
self.seen_active = False
|
||||
self.command_active = False
|
||||
self.last_active_ns = self.active_since_ns = self.acquire_since_ns = 0
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
self.last_sample_ns = self.status_ns
|
||||
# A missing EPS feed must retain a fault even after host health gates emit neutral.
|
||||
if (self.active_since_ns and not self.latched and
|
||||
now_ns - max(self.status_ns, self.active_since_ns) > EPS_STATUS_TIMEOUT_NS):
|
||||
self.latched = True
|
||||
|
||||
recent_command = 0 < self.last_active_ns <= now_ns and now_ns - self.last_active_ns <= STEERING_COMMAND_TIMEOUT_NS
|
||||
eligible = ((healthy or self.status == -1) and self.command_active and recent_command and CS.cruiseState.available and
|
||||
not CS.brakePressed and not CS.regenBraking and not cancel and
|
||||
not CS.standstill and CS.vEgo >= self.min_speed and not CS.steeringPressed)
|
||||
fresh_status = 0 < self.status_ns <= now_ns and now_ns - self.status_ns <= EPS_STATUS_TIMEOUT_NS
|
||||
if eligible and fresh_status and self.status_ns > max(self.active_since_ns, self.command_edge_ns) and self.status_ns != self.last_sample_ns:
|
||||
self.last_sample_ns = self.status_ns
|
||||
if self.status == 1:
|
||||
self.acquire_since_ns = 0
|
||||
self.seen_active = True
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
elif self.status not in (2, 3):
|
||||
if not self.bad_since_ns:
|
||||
self.bad_since_ns = self.status_ns
|
||||
self.bad_samples += 1
|
||||
if self.seen_active and (self.status != 0 or not self.acquire_since_ns):
|
||||
self.latched = True
|
||||
else:
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
# A confirmed neutral acquisition has a fixed deadline, even if repeated edges
|
||||
# place every inactive sample before the latest active command.
|
||||
if eligible and self.acquire_since_ns and now_ns - self.acquire_since_ns >= EPS_STATUS_TIMEOUT_NS:
|
||||
self.latched = True
|
||||
if (eligible and self.bad_samples >= 2 and self.bad_since_ns and
|
||||
now_ns - max(self.active_since_ns, self.acquire_since_ns) >= EPS_STATUS_TIMEOUT_NS):
|
||||
self.latched = True
|
||||
if not recent_command or CS.steeringPressed or CS.standstill or CS.vEgo < self.min_speed:
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
if self.latched:
|
||||
CS.steerFaultTemporary = True
|
||||
@@ -1,9 +1,11 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.gm.carstate import CarState
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.gm.values import CruiseButtons
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.fingerprints import FINGERPRINTS, FW_VERSIONS
|
||||
from opendbc.car.gm.values import ASCM_INTERCEPT_CAR, CAR, DBC, GMSafetyFlags, CarControllerParams
|
||||
@@ -22,6 +24,298 @@ def params(car, *, sascm=False, accelerator=True, radar=False, alpha=False, rele
|
||||
|
||||
|
||||
class TestAscmIntercept(unittest.TestCase):
|
||||
def authority_frame(self, ci, packer, tick, *, status=1, active=True, main=True,
|
||||
regen=False, brake=False, button=CruiseButtons.UNPRESS, driver=0., speed=8., torque=0.02,
|
||||
malformed=False, physical_delay_ns=0, eps_missing=False, now_ns=None, eps_stamp_ns=None, emit_eps=None):
|
||||
now = 1_000_000_000 + tick * 10_000_000 if now_ns is None else now_ns
|
||||
values = {
|
||||
'ECMCruiseControl': {'CruiseActive': 0},
|
||||
'ECMEngineStatus': {'CruiseMainOn': int(main), 'BrakePressed': int(brake)},
|
||||
'AcceleratorPedal2': {'CruiseState': 0},
|
||||
'ECMPRDNL2': {'PRNDL2': 4},
|
||||
'EBCMWheelSpdRear': {'RLWheelSpd': speed * 3.6, 'RRWheelSpd': speed * 3.6, 'RLWheelDir': 1, 'RRWheelDir': 1},
|
||||
'EBCMWheelSpdFront': {'FLWheelSpd': speed * 3.6, 'FRWheelSpd': speed * 3.6},
|
||||
'ESPStatus': {'TractionControlOn': 1},
|
||||
'BCMDoorBeltStatus': {'LeftSeatBelt': 1},
|
||||
'BCMGeneralPlatformStatus': {}, 'BCMTurnSignals': {}, 'PSCMSteeringAngle': {},
|
||||
'ECMAcceleratorPos': {'BrakePedalPos': 30 if brake else 0},
|
||||
'EBCMFrictionBrakeStatus': {}, 'EBCMRegenPaddle': {'RegenPaddle': int(regen)},
|
||||
}
|
||||
packets = [packer.make_can_msg(name, 0, signals) for name, signals in values.items()]
|
||||
packets += [gmcan.create_buttons(packer, 0, tick % 4, button),
|
||||
packer.make_can_msg('ASCMLKASteeringCmd', 2, {}),
|
||||
packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {}),
|
||||
packer.make_can_msg('AEBCmd', 2, {})]
|
||||
eps_packets = []
|
||||
if (tick % 10 == 0 if emit_eps is None else emit_eps) and not eps_missing:
|
||||
pscm = packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': status,
|
||||
'LKADriverAppldTrq': driver, 'LKATorqueDelivered': 0})
|
||||
if malformed:
|
||||
packets.append((0x184, b'\x00', 0))
|
||||
eps_packets.append(pscm)
|
||||
physical = [packet for packet in packets if packet[0] in (0xC9, 0xBE, 0xBD, 0x1E1)]
|
||||
other = [packet for packet in packets if packet[0] not in (0xC9, 0xBE, 0xBD, 0x1E1)]
|
||||
cs = ci.update(sorted([(now - physical_delay_ns, physical), (now, other),
|
||||
(now if eps_stamp_ns is None else eps_stamp_ns, eps_packets)], key=lambda packet: packet[0]))
|
||||
if tick >= 10 and not eps_missing:
|
||||
self.assertTrue(cs.canValid, (tick, status))
|
||||
self.assertFalse(cs.canTimeout, tick)
|
||||
cc = structs.CarControl()
|
||||
cc.enabled = active
|
||||
cc.latActive = active
|
||||
cc.actuators.torque = torque
|
||||
cc.hudControl.setSpeed = 30
|
||||
_, sends = ci.apply(cc.as_reader(), now)
|
||||
steering = [data for address, data, bus in sends if address == 0x180 and bus == 0]
|
||||
if steering:
|
||||
self.assertEqual(bool((steering[-1][0] >> 3) & 1), active and not ci.CS.steering_authority.latched, tick)
|
||||
return cs
|
||||
|
||||
def test_volt_steering_loss_requires_driver_rearm(self):
|
||||
for reset in ('regen', 'brake', 'cancel', 'set', 'main'):
|
||||
with self.subTest(reset=reset):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
# One emitted neutral frame must not erase the confirmed EPS episode.
|
||||
for tick in range(30, 34):
|
||||
self.authority_frame(ci, packer, tick, active=False)
|
||||
for tick in range(34, 41):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
for tick in range(41, 60):
|
||||
cs = self.authority_frame(ci, packer, tick, active=False)
|
||||
self.assertTrue(cs.steerFaultTemporary) # EPS recovery/automatic CC off cannot rearm.
|
||||
for tick in range(60, 70):
|
||||
self.authority_frame(ci, packer, tick, active=False, main=reset != 'main',
|
||||
regen=reset == 'regen', brake=reset == 'brake',
|
||||
button=CruiseButtons.CANCEL if reset == 'cancel' else CruiseButtons.UNPRESS)
|
||||
cs = self.authority_frame(ci, packer, 70, active=False, physical_delay_ns=200_000_000 if reset == 'main' else 1_000_000,
|
||||
button=CruiseButtons.DECEL_SET if reset == 'set' else
|
||||
CruiseButtons.RES_ACCEL if reset != 'main' else CruiseButtons.UNPRESS)
|
||||
if reset in ('set', 'main'):
|
||||
self.assertTrue(cs.steerFaultTemporary, reset)
|
||||
cs = self.authority_frame(ci, packer, 71, active=False)
|
||||
resume_tick = 72
|
||||
if reset == 'set':
|
||||
# SET alone is not the physical disable required to reset a loss latch.
|
||||
self.assertTrue(cs.steerFaultTemporary, reset)
|
||||
self.assertTrue(ci.CS.steering_authority.latched, reset)
|
||||
self.assertEqual(ci.CS.steering_authority.disable_ns, 0, reset)
|
||||
for tick in range(72, 82):
|
||||
self.authority_frame(ci, packer, tick, active=False, regen=True)
|
||||
self.assertGreater(ci.CS.steering_authority.neutral_ns, ci.CS.steering_authority.disable_ns, reset)
|
||||
cs = self.authority_frame(ci, packer, 82, active=False, button=CruiseButtons.DECEL_SET)
|
||||
self.assertTrue(cs.steerFaultTemporary, reset)
|
||||
cs = self.authority_frame(ci, packer, 83, active=False)
|
||||
resume_tick = 84
|
||||
context = (reset, cs.buttonEnable, cs.brakePressed, cs.regenBraking,
|
||||
ci.CS.steering_authority.disable_ns, ci.CS.steering_authority.neutral_ns)
|
||||
self.assertFalse(cs.steerFaultTemporary, context)
|
||||
self.assertFalse(ci.CS.steering_authority.latched, context)
|
||||
for tick in range(resume_tick, resume_tick + 20):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active, context)
|
||||
|
||||
def test_volt_steering_acquisition_and_invalid_status(self):
|
||||
for status, malformed in ((0, False), (4, False), (1, True)):
|
||||
with self.subTest(status=status, malformed=malformed):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
cs = self.authority_frame(ci, packer, tick, status=status, malformed=malformed)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
for tick in range(30, 41):
|
||||
cs = self.authority_frame(ci, packer, tick, status=status, malformed=malformed)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
for tick in range(30, 70):
|
||||
cs = self.authority_frame(ci, packer, tick, active=tick < 40, eps_missing=True)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
for tick in range(70, 90):
|
||||
cs = self.authority_frame(ci, packer, tick, active=False)
|
||||
self.assertTrue(cs.steerFaultTemporary) # Fresh EPS does not erase a missing-feed latch.
|
||||
|
||||
# A total receive gap cannot conceal a stale EPS episode on the first fresh return.
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
cs = self.authority_frame(ci, packer, 70)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
|
||||
def test_volt_steering_healthy_zero_and_existing_faults(self):
|
||||
for status, active, speed, driver in ((1, True, 8., 0.), (0, False, 8., 0.),
|
||||
(0, True, 0., 0.), (0, True, 8., 3.),
|
||||
(2, True, 8., 0.), (3, True, 8., 0.)):
|
||||
with self.subTest(status=status, active=active, speed=speed, driver=driver):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(60):
|
||||
cs = self.authority_frame(ci, packer, tick, status=status, active=active, speed=speed, driver=driver,
|
||||
torque=0. if status == 1 else 0.02)
|
||||
self.assertFalse(ci.CS.steering_authority.latched)
|
||||
self.assertEqual(cs.steerFaultTemporary, status == 2)
|
||||
self.assertEqual(cs.steerFaultPermanent, status == 3)
|
||||
# A genuine driver pause starts a new acquisition window, unlike an unexplained brief CC drop.
|
||||
for pause in ('regen', 'brake', 'cancel', 'main', 'override', 'standstill'):
|
||||
with self.subTest(pause=pause):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
for tick in range(30, 50):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0, active=False,
|
||||
regen=pause == 'regen', brake=pause == 'brake', main=pause != 'main',
|
||||
button=CruiseButtons.CANCEL if pause == 'cancel' else CruiseButtons.UNPRESS,
|
||||
driver=3. if pause == 'override' else 0., speed=0. if pause == 'standstill' else 8.)
|
||||
self.assertFalse(cs.steerFaultTemporary)
|
||||
for tick in range(50, 70):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0 if tick < 65 else 1)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
for tick in range(70, 80):
|
||||
cs = self.authority_frame(ci, packer, tick)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
# Ordinary temporary EPS faults retain their normal recovery/acquisition behavior.
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
for tick in range(30, 50):
|
||||
cs = self.authority_frame(ci, packer, tick, status=2, active=False)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertFalse(ci.CS.steering_authority.latched)
|
||||
for tick in range(50, 80):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0 if tick < 65 else 1)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
for cp in (params(CAR.CHEVROLET_VOLT_ASCM), params(CAR.CHEVROLET_VOLT),
|
||||
params(CAR.CHEVROLET_BOLT_EUV)):
|
||||
self.assertFalse(CarState(cp).steering_authority.enabled)
|
||||
|
||||
def test_volt_steering_neutral_reacquisition(self):
|
||||
for recovered in (True, False):
|
||||
with self.subTest(recovered=recovered):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
for tick in range(30, 90):
|
||||
cs = self.authority_frame(ci, packer, tick, active=False, status=1 if tick < 50 else 0)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
for tick in range(90, 120):
|
||||
cs = self.authority_frame(ci, packer, tick, status=1 if recovered and tick >= 105 else 0)
|
||||
self.assertFalse(cs.steerFaultTemporary, (recovered, tick))
|
||||
for tick in range(120, 131):
|
||||
cs = self.authority_frame(ci, packer, tick, status=1 if recovered else 0)
|
||||
self.assertEqual(cs.steerFaultTemporary, not recovered)
|
||||
self.assertEqual(ci.CS.steering_authority.latched, not recovered)
|
||||
if recovered:
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
else:
|
||||
for tick in range(131, 180):
|
||||
cs = self.authority_frame(ci, packer, tick, active=False)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
for tick in range(180, 200):
|
||||
cs = self.authority_frame(ci, packer, tick)
|
||||
self.assertTrue(cs.steerFaultTemporary, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
|
||||
def test_volt_route24_eps_sample_before_resumed_command(self):
|
||||
# Route 24: 163.588 ms between active sends, 131.971 ms of emitted neutral.
|
||||
# Exercise the actual interface/parser/controller path; do not assign monitor history.
|
||||
for subsequent_status in (1, 0, 3, -1, "edge-zero"):
|
||||
with self.subTest(subsequent_status=subsequent_status):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick, now_ns=232_790_000_000 + tick * 10_000_000)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
timeline = ((233_179_157_875, True, None, 1),
|
||||
(233_195_000_000, True, 233_182_155_170, 1),
|
||||
(233_203_000_000, False, None, 1),
|
||||
(233_210_774_096, False, None, 1),
|
||||
(233_230_000_000, False, None, 1),
|
||||
(233_243_000_000, False, None, 1),
|
||||
(233_253_611_922, False, None, 1),
|
||||
(233_267_000_000, False, None, 1),
|
||||
(233_274_000_000, False, None, 1),
|
||||
(233_281_143_669, False, None, 1),
|
||||
(233_298_000_000, False, 233_282_094_035, 0),
|
||||
(233_306_000_000, False, None, 0),
|
||||
(233_314_029_085, False, None, 0),
|
||||
(233_328_000_000, True, None, 0),
|
||||
(233_335_000_000, True, None, 0),
|
||||
(233_342_745_540, True, None, 0),
|
||||
(233_346_666_621, True, None, 0))
|
||||
for tick, (now, active, stamp, status) in enumerate(timeline, 30):
|
||||
cs = self.authority_frame(ci, packer, tick, now_ns=now, active=active, status=status,
|
||||
emit_eps=stamp is not None, eps_stamp_ns=stamp)
|
||||
self.assertFalse(ci.CS.steering_authority.latched, (now, stamp))
|
||||
self.assertFalse(cs.steerFaultTemporary, now)
|
||||
monitor = ci.CS.steering_authority
|
||||
self.assertTrue(monitor.seen_active) # Short neutral retains genuine fault history.
|
||||
self.assertEqual(monitor.command_edge_ns, 233_342_745_540)
|
||||
self.assertEqual(monitor.status_ns, 233_282_094_035)
|
||||
self.assertEqual(monitor.acquire_since_ns, 233_342_745_540)
|
||||
if subsequent_status == "edge-zero":
|
||||
# Every 10Hz zero arrives during emitted neutral, before the next resume.
|
||||
# Thus no post-edge bad sample is counted; short edges must not renew 300ms.
|
||||
self.authority_frame(ci, packer, 47, now_ns=233_360_000_000, active=False, emit_eps=False)
|
||||
self.authority_frame(ci, packer, 48, now_ns=233_370_000_000, active=False, emit_eps=False)
|
||||
self.authority_frame(ci, packer, 49, now_ns=233_386_000_000, active=False,
|
||||
status=0, emit_eps=True, eps_stamp_ns=233_382_063_562)
|
||||
for tick in range(50, 76):
|
||||
now = 233_386_000_000 + (tick - 49) * 10_000_000
|
||||
active = 50 <= tick <= 52 or 60 <= tick <= 61 or tick >= 70
|
||||
cs = self.authority_frame(ci, packer, tick, now_ns=now, active=active,
|
||||
status=0, emit_eps=tick in (59, 69),
|
||||
eps_stamp_ns=now - 5_000_000)
|
||||
self.assertEqual(monitor.acquire_since_ns, 233_342_745_540, now)
|
||||
self.assertEqual(monitor.bad_samples, 0, now)
|
||||
self.assertEqual(monitor.latched, now >= 233_642_745_540, now)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
continue
|
||||
# The next measured inactive status arrives 39ms after resume; normal response
|
||||
# is active at 138ms. A permanently inactive response must still fault at 300ms.
|
||||
cs = self.authority_frame(ci, packer, 47, now_ns=233_386_000_000,
|
||||
status=0, emit_eps=True, eps_stamp_ns=233_382_063_562)
|
||||
self.assertFalse(monitor.latched)
|
||||
self.assertFalse(cs.steerFaultTemporary)
|
||||
for tick in range(48, 57):
|
||||
self.authority_frame(ci, packer, tick, now_ns=233_386_000_000 + (tick - 47) * 10_000_000,
|
||||
emit_eps=False)
|
||||
cs = self.authority_frame(ci, packer, 57, now_ns=233_486_000_000,
|
||||
status=max(0, subsequent_status), malformed=subsequent_status == -1,
|
||||
emit_eps=True, eps_stamp_ns=233_481_278_455)
|
||||
self.assertEqual(monitor.latched, subsequent_status == -1)
|
||||
self.assertEqual(cs.steerFaultPermanent, subsequent_status == 3)
|
||||
if subsequent_status in (3, -1):
|
||||
continue
|
||||
for tick in range(58, 74):
|
||||
now = 233_486_000_000 + (tick - 57) * 10_000_000
|
||||
cs = self.authority_frame(ci, packer, tick, now_ns=now,
|
||||
status=subsequent_status, emit_eps=tick == 67,
|
||||
active=not (subsequent_status == 0 and 62 <= tick < 69))
|
||||
if subsequent_status == 0:
|
||||
self.assertEqual(monitor.acquire_since_ns, 233_342_745_540, now)
|
||||
self.assertEqual(monitor.latched, subsequent_status == 0 and now >= 233_642_745_540, now)
|
||||
self.assertEqual(cs.steerFaultTemporary, subsequent_status == 0)
|
||||
self.assertTrue(monitor.seen_active)
|
||||
|
||||
def test_eight_manual_ids_have_stock_acc_default(self):
|
||||
self.assertEqual(len(ASCM_INTERCEPT_CAR), 8)
|
||||
for car in ASCM_INTERCEPT_CAR:
|
||||
@@ -34,7 +328,7 @@ class TestAscmIntercept(unittest.TestCase):
|
||||
self.assertAlmostEqual(cp.minEnableSpeed, -1. if car == CAR.CADILLAC_ESCALADE_ESV_2019_ASCM else 5 / 3.6, places=6)
|
||||
self.assertTrue(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.ASCM_INTERCEPT)
|
||||
self.assertFalse(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.HW_CAM_LONG)
|
||||
self.assertEqual(car.config.car_docs, [])
|
||||
self.assertEqual(car.config.car_docs[0].support_type.value, "Community")
|
||||
self.assertNotIn(car, FINGERPRINTS)
|
||||
self.assertNotIn(car, FW_VERSIONS)
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class TestCcGatewayStock(unittest.TestCase):
|
||||
def test_manual_identity_and_stock_ownership(self):
|
||||
self.assertEqual(len(CC_GATEWAY_STOCK_CAR), 9)
|
||||
for car in CC_GATEWAY_STOCK_CAR:
|
||||
self.assertEqual(car.config.car_docs, [])
|
||||
self.assertEqual(car.config.car_docs[0].support_type.value, "Community")
|
||||
automatic = {CAR.CADILLAC_CT6_CC, CAR.CADILLAC_XT5_CC, CAR.CHEVROLET_EQUINOX_CC,
|
||||
CAR.CHEVROLET_MALIBU_CC, CAR.CHEVROLET_SUBURBAN_CC, CAR.CHEVROLET_TRAILBLAZER_CC}
|
||||
self.assertEqual(car in FINGERPRINTS, car in automatic)
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestSdgmStock(unittest.TestCase):
|
||||
self.assertTrue(flags & GMSafetyFlags.SDGM)
|
||||
self.assertFalse(flags & GMSafetyFlags.HW_CAM_LONG)
|
||||
self.assertEqual(cp.radarUnavailable, not radar)
|
||||
self.assertEqual(car.config.car_docs, [])
|
||||
self.assertEqual(car.config.car_docs[0].support_type.value, "Community")
|
||||
self.assertNotIn(car, FINGERPRINTS)
|
||||
self.assertNotIn(car, FW_VERSIONS)
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ class TestSuburbanGateway(unittest.TestCase):
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.gm.carcontroller import CarController
|
||||
from opendbc.car.gm.carstate import CarState
|
||||
fp = gen_empty_fingerprint()
|
||||
fp[1][0x460] = 8
|
||||
cp = CarInterface.get_params(CAR.CHEVROLET_SUBURBAN, fp, [], False, False, False)
|
||||
@@ -72,7 +73,8 @@ class TestSuburbanGateway(unittest.TestCase):
|
||||
state = structs.CarState(vEgo=12)
|
||||
state.cruiseState.available = True
|
||||
cs = SimpleNamespace(out=state.as_reader(), cam_lka_steering_cmd_counter=0, loopback_lka_steering_cmd_updated=False,
|
||||
loopback_lka_steering_cmd_ts_nanos=1_000_000_000, pt_lka_steering_cmd_counter=0)
|
||||
loopback_lka_steering_cmd_ts_nanos=1_000_000_000, pt_lka_steering_cmd_counter=0,
|
||||
steering_authority=CarState(cp).steering_authority)
|
||||
seen = 0
|
||||
for frame in range(100):
|
||||
control = structs.CarControl(enabled=True, latActive=True, longActive=True)
|
||||
|
||||
@@ -98,6 +98,18 @@ class TestVoltAlternateBrake(unittest.TestCase):
|
||||
brake=brake, gas=gas, camera=cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera and
|
||||
not is_volt_camera_removed(cp))
|
||||
packets = [packet for packet in packets if packet[0] not in (0xBE, 0xF1, 0x1C4)]
|
||||
if ci.CS.steering_authority.enabled:
|
||||
packets = [packet for packet in packets if packet[0] != 0x184]
|
||||
eps_status = 1 if active else 0
|
||||
packets.append(packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': eps_status}))
|
||||
# The forwarded EPS status reflects the healthy input; all other frozen bytes stay exact.
|
||||
expected = [message.copy() for message in expected]
|
||||
for message in expected:
|
||||
if message[0] == 0x184:
|
||||
data = bytearray.fromhex(message[1])
|
||||
self.assertEqual(data[0] & 0x38, 0)
|
||||
data[0] |= eps_status << 3
|
||||
message[1] = data.hex()
|
||||
packets.append(packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 2 if active else 0,
|
||||
'AcceleratorPedal2': 30 if gas else 0}))
|
||||
if be:
|
||||
|
||||
@@ -5,7 +5,7 @@ from types import MappingProxyType
|
||||
|
||||
from opendbc.car import Bus, PlatformConfig, DbcDict, Platforms, CarSpecs
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.docs_definitions import CarDocs, CarFootnote, CarHarness, CarParts, Column
|
||||
from opendbc.car.docs_definitions import CarDocs, CarFootnote, CarHarness, CarParts, Column, SupportType
|
||||
from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
@@ -140,6 +140,14 @@ class GMFlags(IntFlag):
|
||||
VOLT_CAMERA_NO_ACCEL_POS = NO_ACCELERATOR_POS_MSG
|
||||
|
||||
|
||||
_GM_CONTROL_FLAGS_MASK = ~int(GMFlags.HAS_BSM)
|
||||
_GM_PEDAL_LONG_FLAG = int(GMFlags.PEDAL_LONG)
|
||||
_GM_NO_CAMERA_FLAG = int(GMFlags.NO_CAMERA)
|
||||
_GM_NO_ACCELERATOR_POS_FLAG = int(GMFlags.NO_ACCELERATOR_POS_MSG)
|
||||
_BOLT_REMOVED_CONTROL_FLAGS = _GM_PEDAL_LONG_FLAG | _GM_NO_CAMERA_FLAG
|
||||
_CC_PEDAL_ALLOWED_FLAGS = _BOLT_REMOVED_CONTROL_FLAGS | _GM_NO_ACCELERATOR_POS_FLAG
|
||||
|
||||
|
||||
VOLT_ONE_PEDAL_BASE_WORDS = (0x4084, 0xC084, 0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0x5087, 0x5487, 0xC1D1, 0xC1D3)
|
||||
VOLT_ONE_PEDAL_WORDS = {start + index: word for start in (0xD100, 0xD110)
|
||||
for index, word in enumerate(VOLT_ONE_PEDAL_BASE_WORDS)}
|
||||
@@ -159,7 +167,7 @@ def gm_control_word(cp: CarParams) -> int:
|
||||
hybrid = malibu_hybrid_profile(cp)
|
||||
if hybrid is not None:
|
||||
return 5 | (8 if hybrid.pedal and hybrid.longitudinal else 0)
|
||||
if is_bolt_pedal_removed_profile(cp) or is_bolt_pedal_removed_profile(cp, stock_only=True):
|
||||
if word in _BOLT_REMOVED_SAFETY_WORDS and (is_bolt_pedal_removed_profile(cp) or is_bolt_pedal_removed_profile(cp, stock_only=True)):
|
||||
return int(BOLT_PEDAL_STOCK_WORDS[cp.carFingerprint] if not cp.openpilotLongitudinalControl else BOLT_PEDAL_WORDS[cp.carFingerprint])
|
||||
if word in VOLT_CC_PEDAL_PROFILES and volt_cc_pedal_profile(cp) is not None:
|
||||
return 5
|
||||
@@ -217,7 +225,7 @@ def apply_volt_one_pedal(cp: CarParams, enabled: bool, auto_hold_enabled: bool)
|
||||
|
||||
def control_flags(cp: CarParams) -> int:
|
||||
"""Exclude only the informational BSM bit from exact control-profile admission."""
|
||||
flags = int(cp.flags) & ~int(GMFlags.HAS_BSM)
|
||||
flags = int(cp.flags) & _GM_CONTROL_FLAGS_MASK
|
||||
profile = camera_acc_pedal_profile(cp)
|
||||
if profile is not None and profile.volt:
|
||||
flags &= ~int(GMFlags.PEDAL_LONG)
|
||||
@@ -451,7 +459,10 @@ class GMCarDocs(CarDocs):
|
||||
package: str = "Adaptive Cruise Control (ACC)"
|
||||
|
||||
def init_make(self, CP: CarParams):
|
||||
if CP.networkLocation == CarParams.NetworkLocation.fwdCamera:
|
||||
if (CP.carFingerprint in ASCM_INTERCEPT_CAR or CP.carFingerprint in CC_GATEWAY_STOCK_CAR or
|
||||
CP.carFingerprint in (CAR.CHEVROLET_VOLT_CC, CAR.CHEVROLET_MALIBU_HYBRID_CC)):
|
||||
self.car_parts = CarParts.common([CarHarness.custom])
|
||||
elif CP.networkLocation == CarParams.NetworkLocation.fwdCamera:
|
||||
if CP.carFingerprint in SDGM_CAR:
|
||||
self.car_parts = CarParts.common([CarHarness.gmsdgm])
|
||||
else:
|
||||
@@ -461,6 +472,13 @@ class GMCarDocs(CarDocs):
|
||||
self.car_parts = CarParts.common([CarHarness.obd_ii])
|
||||
|
||||
|
||||
@dataclass
|
||||
class GMCommunityCarDocs(GMCarDocs):
|
||||
merged: bool = False
|
||||
support_type: SupportType = SupportType.COMMUNITY
|
||||
support_link: str = "#community"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GMCarSpecs(CarSpecs):
|
||||
tireStiffnessFactor: float = 0.444 # not optimized yet
|
||||
@@ -475,105 +493,81 @@ class GMPlatformConfig(PlatformConfig):
|
||||
})
|
||||
|
||||
|
||||
@dataclass
|
||||
class GMASCMPlatformConfig(GMPlatformConfig):
|
||||
def init(self):
|
||||
# ASCM is supported, but due to a janky install and hardware configuration, we are not showing in the car docs
|
||||
self.car_docs = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class GMSDGMPlatformConfig(GMPlatformConfig):
|
||||
def init(self):
|
||||
# Don't show in docs until the harness is sold. See https://github.com/commaai/openpilot/issues/32471
|
||||
self.car_docs = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class GMCCGatewayPlatformConfig(GMPlatformConfig):
|
||||
def init(self):
|
||||
# Manual-only identities; installation and route evidence is not available.
|
||||
self.car_docs = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class GMCameraStockPlatformConfig(GMPlatformConfig):
|
||||
def init(self):
|
||||
# Explicit identities pending independent install and route qualification.
|
||||
self.car_docs = []
|
||||
|
||||
|
||||
class CAR(Platforms):
|
||||
HOLDEN_ASTRA = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Holden Astra 2017")],
|
||||
HOLDEN_ASTRA = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Holden Astra (OBD-II Harness) 2017")],
|
||||
GMCarSpecs(mass=1363, wheelbase=2.662, steerRatio=15.7, centerToFrontRatio=0.4),
|
||||
)
|
||||
CHEVROLET_VOLT = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Volt 2017-18", min_enable_speed=0, video="https://youtu.be/QeMCN_4TFfQ")],
|
||||
CHEVROLET_VOLT = GMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Volt (OBD-II Harness) 2017-18", support_type=SupportType.COMMUNITY, support_link="#community",
|
||||
min_enable_speed=0, video="https://youtu.be/QeMCN_4TFfQ")],
|
||||
GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CHEVROLET_VOLT_CC = GMPlatformConfig(
|
||||
[], # Manual development identity; no fingerprint, firmware alias or public driving claim.
|
||||
[GMCarDocs("Chevrolet Volt (CC)", "Manual development configuration; installation must be verified",
|
||||
merged=False, support_type=SupportType.COMMUNITY, support_link="#community")],
|
||||
CHEVROLET_VOLT.specs, dbc_dict=CHEVROLET_VOLT.dbc_dict,
|
||||
)
|
||||
CHEVROLET_VOLT_ASCM = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Volt ASCM Harness 2017-18")], CHEVROLET_VOLT.specs,
|
||||
CHEVROLET_VOLT_ASCM = GMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Volt (ASCM Harness) 2017-18", "Adaptive Cruise Control (ACC); custom ASCM harness install",
|
||||
support_type=SupportType.COMMUNITY, support_link="#community")], CHEVROLET_VOLT.specs,
|
||||
)
|
||||
CADILLAC_ATS = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Cadillac ATS Premium Performance 2018")],
|
||||
CADILLAC_ATS = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac ATS Premium Performance (OBD-II Harness) 2018")],
|
||||
GMCarSpecs(mass=1601, wheelbase=2.78, steerRatio=15.3),
|
||||
)
|
||||
CHEVROLET_MALIBU = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Malibu Premier 2017")],
|
||||
CHEVROLET_MALIBU = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Malibu Premier (OBD-II Harness) 2017")],
|
||||
GMCarSpecs(mass=1496, wheelbase=2.83, steerRatio=15.8, centerToFrontRatio=0.4),
|
||||
)
|
||||
CHEVROLET_MALIBU_ASCM = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Malibu ASCM Harness 2017-19")], CHEVROLET_MALIBU.specs,
|
||||
CHEVROLET_MALIBU_ASCM = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Malibu (ASCM Harness) 2017-19")], CHEVROLET_MALIBU.specs,
|
||||
)
|
||||
GMC_ACADIA = GMASCMPlatformConfig(
|
||||
[GMCarDocs("GMC Acadia 2018", video="https://www.youtube.com/watch?v=0ZN6DdsBUZo")],
|
||||
GMC_ACADIA = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("GMC Acadia (OBD-II Harness) 2018", video="https://www.youtube.com/watch?v=0ZN6DdsBUZo")],
|
||||
GMCarSpecs(mass=1975, wheelbase=2.86, steerRatio=14.4, centerToFrontRatio=0.4),
|
||||
)
|
||||
GMC_ACADIA_ASCM = GMASCMPlatformConfig(
|
||||
[GMCarDocs("GMC Acadia ASCM Harness 2018")], GMC_ACADIA.specs,
|
||||
GMC_ACADIA_ASCM = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("GMC Acadia (ASCM Harness) 2018")], GMC_ACADIA.specs,
|
||||
)
|
||||
BUICK_LACROSSE = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Buick LaCrosse 2017-19", "Driver Confidence Package 2")],
|
||||
BUICK_LACROSSE = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Buick LaCrosse (OBD-II Harness) 2017-19", "Driver Confidence Package 2")],
|
||||
GMCarSpecs(mass=1712, wheelbase=2.91, steerRatio=15.8, centerToFrontRatio=0.4),
|
||||
)
|
||||
BUICK_LACROSSE_ASCM = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Buick LaCrosse ASCM Harness 2017-19")], BUICK_LACROSSE.specs,
|
||||
BUICK_LACROSSE_ASCM = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Buick LaCrosse (ASCM Harness) 2017-19")], BUICK_LACROSSE.specs,
|
||||
)
|
||||
BUICK_LACROSSE_ASCM_19US = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Buick LaCrosse US ASCM Harness 2019")], BUICK_LACROSSE.specs,
|
||||
BUICK_LACROSSE_ASCM_19US = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Buick LaCrosse US (ASCM Harness) 2019")], BUICK_LACROSSE.specs,
|
||||
)
|
||||
BUICK_REGAL = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Buick Regal Essence 2018")],
|
||||
BUICK_REGAL = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Buick Regal Essence (OBD-II Harness) 2018")],
|
||||
GMCarSpecs(mass=1714, wheelbase=2.83, steerRatio=14.4, centerToFrontRatio=0.4),
|
||||
)
|
||||
CADILLAC_ESCALADE = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Cadillac Escalade 2017", "Driver Assist Package")],
|
||||
CADILLAC_ESCALADE = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac Escalade (OBD-II Harness) 2017", "Driver Assist Package")],
|
||||
GMCarSpecs(mass=2564, wheelbase=2.95, steerRatio=17.3),
|
||||
)
|
||||
CADILLAC_ESCALADE_ASCM = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Cadillac Escalade ASCM Harness 2018")], CADILLAC_ESCALADE.specs,
|
||||
CADILLAC_ESCALADE_ASCM = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac Escalade (ASCM Harness) 2018")], CADILLAC_ESCALADE.specs,
|
||||
)
|
||||
CADILLAC_ESCALADE_ESV = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Cadillac Escalade ESV 2016", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
CADILLAC_ESCALADE_ESV = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac Escalade ESV (OBD-II Harness) 2016", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
GMCarSpecs(mass=2739, wheelbase=3.302, steerRatio=17.3, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CADILLAC_ESCALADE_ESV_2019 = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Cadillac Escalade ESV 2019", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
CADILLAC_ESCALADE_ESV_2019 = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac Escalade ESV (OBD-II Harness) 2019", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
CADILLAC_ESCALADE_ESV.specs,
|
||||
)
|
||||
CADILLAC_ESCALADE_ESV_2019_ASCM = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Cadillac Escalade ESV Platinum ASCM Harness 2019")], CADILLAC_ESCALADE_ESV_2019.specs,
|
||||
CADILLAC_ESCALADE_ESV_2019_ASCM = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac Escalade ESV Platinum (ASCM Harness) 2019")], CADILLAC_ESCALADE_ESV_2019.specs,
|
||||
)
|
||||
CHEVROLET_SUBURBAN = GMPlatformConfig(
|
||||
[], CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
|
||||
[GMCommunityCarDocs("Chevrolet Suburban (OBD-II Harness)", "Configuration-specific install; observed CAN sources required")],
|
||||
CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
|
||||
)
|
||||
CHEVROLET_SUBURBAN_ASCM = GMASCMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Suburban Premier ASCM Harness 2016-20")],
|
||||
CHEVROLET_SUBURBAN_ASCM = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Suburban Premier (ASCM Harness) 2016-20")],
|
||||
CHEVROLET_SUBURBAN.specs,
|
||||
)
|
||||
CHEVROLET_BOLT_EUV = GMPlatformConfig(
|
||||
@@ -583,7 +577,10 @@ class CAR(Platforms):
|
||||
],
|
||||
GMCarSpecs(mass=1669, wheelbase=2.63779, steerRatio=16.8, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CHEVROLET_BOLT_ACC_2022_2023 = GMCameraStockPlatformConfig([], CHEVROLET_BOLT_EUV.specs)
|
||||
CHEVROLET_BOLT_ACC_2022_2023 = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Bolt (Camera Harness) 2022-23", "ACC; recognized camera layout required")],
|
||||
CHEVROLET_BOLT_EUV.specs,
|
||||
)
|
||||
CHEVROLET_BOLT_CC_2017 = GMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Bolt EV 2017", "Cruise control with installed pedal interceptor")],
|
||||
CHEVROLET_BOLT_EUV.specs,
|
||||
@@ -617,63 +614,95 @@ class CAR(Platforms):
|
||||
[GMCarDocs("Chevrolet Trailblazer 2021-22")],
|
||||
GMCarSpecs(mass=1345, wheelbase=2.64, steerRatio=16.8, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CADILLAC_XT4 = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Cadillac XT4 2023", "Driver Assist Package")],
|
||||
CADILLAC_XT4 = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac XT4 (SDGM Harness) 2023", "Driver Assist Package")],
|
||||
GMCarSpecs(mass=1660, wheelbase=2.78, steerRatio=14.4, centerToFrontRatio=0.4),
|
||||
)
|
||||
CADILLAC_XT5 = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Cadillac XT5 2022", "Driver Assist Package")],
|
||||
CADILLAC_XT5 = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac XT5 (SDGM Harness) 2022", "Driver Assist Package")],
|
||||
GMCarSpecs(mass=1810, wheelbase=2.86, steerRatio=16.34, centerToFrontRatio=0.5, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CADILLAC_XT6 = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Cadillac XT6 2020", "Driver Assist Package")],
|
||||
CADILLAC_XT6 = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac XT6 (SDGM Harness) 2020", "Driver Assist Package")],
|
||||
GMCarSpecs(mass=2050, wheelbase=2.86, steerRatio=16.5, centerToFrontRatio=0.4),
|
||||
)
|
||||
CHEVROLET_BLAZER = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Blazer 2019-25", "Driver Assist Package")],
|
||||
CHEVROLET_BLAZER = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Blazer (SDGM Harness) 2019-25", "Driver Assist Package")],
|
||||
GMCarSpecs(mass=1850, wheelbase=3.10, steerRatio=17.9, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CHEVROLET_VOLT_2019 = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Volt 2019", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
CHEVROLET_VOLT_2019 = GMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Volt (SDGM Harness) 2019", "Adaptive Cruise Control (ACC) & LKAS",
|
||||
support_type=SupportType.COMMUNITY, support_link="#community")],
|
||||
GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45),
|
||||
)
|
||||
CHEVROLET_TRAVERSE = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Traverse 2022-23", "RS, Premier, or High Country Trim")],
|
||||
CHEVROLET_TRAVERSE = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Traverse (SDGM Harness) 2022-23", "RS, Premier, or High Country Trim")],
|
||||
GMCarSpecs(mass=1955, wheelbase=3.07, steerRatio=17.9, centerToFrontRatio=0.4),
|
||||
)
|
||||
CHEVROLET_MALIBU_SDGM = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Malibu 2019", "SDGM Harness")],
|
||||
CHEVROLET_MALIBU_SDGM = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Malibu (SDGM Harness) 2019", "SDGM Harness")],
|
||||
CHEVROLET_MALIBU.specs,
|
||||
)
|
||||
BUICK_BABYENCLAVE = GMSDGMPlatformConfig(
|
||||
[GMCarDocs("Buick Baby Enclave 2020-23", "Driver Assist Package")],
|
||||
BUICK_BABYENCLAVE = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Buick Baby Enclave (SDGM Harness) 2020-23", "Driver Assist Package")],
|
||||
GMCarSpecs(mass=2050, wheelbase=2.86, steerRatio=16.0, centerToFrontRatio=0.5, tireStiffnessFactor=1.0),
|
||||
)
|
||||
GMC_YUKON = GMPlatformConfig(
|
||||
[GMCarDocs("GMC Yukon 2019-20", "Adaptive Cruise Control (ACC) & LKAS")],
|
||||
GMCarSpecs(mass=2490, wheelbase=2.94, steerRatio=17.3, centerToFrontRatio=0.5, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CHEVROLET_SUBURBAN_CAMERA = GMCameraStockPlatformConfig(
|
||||
[], CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
|
||||
CHEVROLET_SUBURBAN_CAMERA = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Suburban (Camera Harness)", "Manual camera configuration; recognized CAN layout required")],
|
||||
CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
|
||||
)
|
||||
CHEVROLET_TRAX = GMCameraStockPlatformConfig(
|
||||
[], CarSpecs(mass=1365, wheelbase=2.7, steerRatio=16.4, centerToFrontRatio=0.4),
|
||||
CHEVROLET_TRAX = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Trax (Camera Harness)", "Manual camera configuration; recognized CAN layout required")],
|
||||
CarSpecs(mass=1365, wheelbase=2.7, steerRatio=16.4, centerToFrontRatio=0.4),
|
||||
)
|
||||
CHEVROLET_VOLT_CAMERA = GMCameraStockPlatformConfig(
|
||||
[], GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45, tireStiffnessFactor=1.0),
|
||||
CHEVROLET_VOLT_CAMERA = GMPlatformConfig(
|
||||
[GMCarDocs("Chevrolet Volt (Camera Harness)", "Manual configuration; recognized camera layout required",
|
||||
merged=False, support_type=SupportType.COMMUNITY, support_link="#community")],
|
||||
GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45, tireStiffnessFactor=1.0),
|
||||
dbc_dict=CHEVROLET_VOLT.dbc_dict,
|
||||
)
|
||||
CADILLAC_CT6_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=2358, wheelbase=3.11, steerRatio=17.7, centerToFrontRatio=0.4, tireStiffnessFactor=1.0))
|
||||
CADILLAC_XT4_CC = GMCCGatewayPlatformConfig([], CADILLAC_XT4.specs)
|
||||
CADILLAC_XT5_CC = GMCCGatewayPlatformConfig([], replace(CADILLAC_XT5.specs, tireStiffnessFactor=1.0))
|
||||
CHEVROLET_EQUINOX_CC = GMCCGatewayPlatformConfig([], CHEVROLET_EQUINOX.specs)
|
||||
CHEVROLET_MALIBU_HYBRID_CC = GMPlatformConfig([], GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=15.8, centerToFrontRatio=0.4))
|
||||
CHEVROLET_MALIBU_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=18.25,
|
||||
centerToFrontRatio=0.4, tireStiffnessFactor=0.997))
|
||||
CHEVROLET_SILVERADO_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=2994, wheelbase=3.75, steerRatio=16.3, tireStiffnessFactor=1.0))
|
||||
CHEVROLET_SUBURBAN_CC = GMCCGatewayPlatformConfig([], replace(CHEVROLET_SUBURBAN_ASCM.specs, tireStiffnessFactor=1.0))
|
||||
CHEVROLET_TRAILBLAZER_CC = GMCCGatewayPlatformConfig([], CHEVROLET_TRAILBLAZER.specs)
|
||||
GMC_YUKON_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=2541, wheelbase=2.95, steerRatio=16.3, centerToFrontRatio=0.4, tireStiffnessFactor=1.0))
|
||||
CADILLAC_CT6_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac CT6 (CC)", "Cruise control; configuration-specific installation")],
|
||||
GMCarSpecs(mass=2358, wheelbase=3.11, steerRatio=17.7, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CADILLAC_XT4_CC = GMPlatformConfig([GMCommunityCarDocs("Cadillac XT4 (CC)", "Cruise control; configuration-specific installation")], CADILLAC_XT4.specs)
|
||||
CADILLAC_XT5_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Cadillac XT5 (CC)", "Cruise control; configuration-specific installation")],
|
||||
replace(CADILLAC_XT5.specs, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CHEVROLET_EQUINOX_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Equinox (CC)", "Cruise control; configuration-specific installation")],
|
||||
CHEVROLET_EQUINOX.specs,
|
||||
)
|
||||
CHEVROLET_MALIBU_HYBRID_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Malibu Hybrid (CC)", "Cruise control; configuration-specific installation")],
|
||||
GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=15.8, centerToFrontRatio=0.4),
|
||||
)
|
||||
CHEVROLET_MALIBU_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Malibu (CC)", "Cruise control; configuration-specific installation")],
|
||||
GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=18.25,
|
||||
centerToFrontRatio=0.4, tireStiffnessFactor=0.997),
|
||||
)
|
||||
CHEVROLET_SILVERADO_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Silverado (CC)", "Cruise control; configuration-specific installation")],
|
||||
GMCarSpecs(mass=2994, wheelbase=3.75, steerRatio=16.3, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CHEVROLET_SUBURBAN_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Suburban (CC)", "Cruise control; configuration-specific installation")],
|
||||
replace(CHEVROLET_SUBURBAN_ASCM.specs, tireStiffnessFactor=1.0),
|
||||
)
|
||||
CHEVROLET_TRAILBLAZER_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("Chevrolet Trailblazer (CC)", "Cruise control; configuration-specific installation")],
|
||||
CHEVROLET_TRAILBLAZER.specs,
|
||||
)
|
||||
GMC_YUKON_CC = GMPlatformConfig(
|
||||
[GMCommunityCarDocs("GMC Yukon (CC)", "Cruise control; configuration-specific installation")],
|
||||
GMCarSpecs(mass=2541, wheelbase=2.95, steerRatio=16.3, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
|
||||
)
|
||||
|
||||
|
||||
class CruiseButtons:
|
||||
@@ -846,16 +875,18 @@ BOLT_PEDAL_REMOVED_CARS = (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_201
|
||||
CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL)
|
||||
BOLT_PEDAL_REMOVED_WORDS = MappingProxyType({candidate: 0xE700 + index for index, candidate in enumerate(BOLT_PEDAL_REMOVED_CARS)})
|
||||
BOLT_PEDAL_REMOVED_STOCK_WORDS = MappingProxyType({candidate: 0xE710 + index for index, candidate in enumerate(BOLT_PEDAL_REMOVED_CARS)})
|
||||
_BOLT_REMOVED_SAFETY_WORDS = frozenset((*BOLT_PEDAL_REMOVED_WORDS.values(), *BOLT_PEDAL_REMOVED_STOCK_WORDS.values()))
|
||||
|
||||
|
||||
def is_bolt_pedal_removed_profile(cp, *, stock_only=False):
|
||||
words = BOLT_PEDAL_REMOVED_STOCK_WORDS if stock_only else BOLT_PEDAL_REMOVED_WORDS
|
||||
try:
|
||||
return (cp.brand == 'gm' and cp.carFingerprint in words and cp.transmissionType == CarParams.TransmissionType.direct and
|
||||
return (cp.brand == 'gm' and cp.carFingerprint in words and
|
||||
int(cp.flags) & _GM_CONTROL_FLAGS_MASK == _BOLT_REMOVED_CONTROL_FLAGS and
|
||||
cp.transmissionType == CarParams.TransmissionType.direct and
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.radarUnavailable and
|
||||
not (cp.passive or cp.dashcamOnly or cp.notCar or cp.alphaLongitudinalAvailable) and
|
||||
bool(cp.openpilotLongitudinalControl) is not stock_only and bool(cp.pcmCruise) is stock_only and
|
||||
int(cp.flags) & ~int(GMFlags.HAS_BSM) == int(GMFlags.PEDAL_LONG | GMFlags.NO_CAMERA) and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
int(cp.safetyConfigs[0].safetyParam) == words[cp.carFingerprint])
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
@@ -871,7 +902,7 @@ def is_bolt_pedal_profile(cp, *, stock_only=False):
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and
|
||||
not (cp.passive or cp.dashcamOnly or cp.notCar or cp.alphaLongitudinalAvailable) and
|
||||
bool(cp.openpilotLongitudinalControl) is not stock_only and bool(cp.pcmCruise) is stock_only and
|
||||
control_flags(cp) == GMFlags.PEDAL_LONG.value and len(cp.safetyConfigs) == 1 and
|
||||
int(cp.flags) & _GM_CONTROL_FLAGS_MASK == _GM_PEDAL_LONG_FLAG and len(cp.safetyConfigs) == 1 and
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
cp.safetyConfigs[0].safetyParam == words[cp.carFingerprint])
|
||||
|
||||
@@ -1071,13 +1102,15 @@ def volt_camera_pedal_word(profile, auto_hold: bool, one_pedal: bool) -> int:
|
||||
|
||||
def camera_acc_pedal_profile(cp):
|
||||
try:
|
||||
if cp.brand != 'gm' or cp.carFingerprint not in CAMERA_ACC_PEDAL_CAR:
|
||||
return None
|
||||
if len(cp.safetyConfigs) != 1:
|
||||
return None
|
||||
profile = CAMERA_ACC_PEDAL_PROFILES.get(int(cp.safetyConfigs[0].safetyParam))
|
||||
if profile is None:
|
||||
return None
|
||||
flags = int(GMFlags.PEDAL_LONG | (GMFlags.NO_CAMERA if profile.removed else 0) |
|
||||
(GMFlags.NO_ACCELERATOR_POS_MSG if profile.brake_source == BrakeSource.F1 else 0))
|
||||
flags = (_GM_PEDAL_LONG_FLAG | (_GM_NO_CAMERA_FLAG if profile.removed else 0) |
|
||||
(_GM_NO_ACCELERATOR_POS_FLAG if profile.brake_source == BrakeSource.F1 else 0))
|
||||
identity = (CAR.CHEVROLET_VOLT if profile.topology == "gateway" else
|
||||
CAR.CHEVROLET_VOLT_ASCM if profile.topology == "ascm" else
|
||||
CAR.CHEVROLET_VOLT_2019 if profile.topology == "sdgm" else CAR.CHEVROLET_VOLT_CAMERA)
|
||||
@@ -1086,7 +1119,7 @@ def camera_acc_pedal_profile(cp):
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and (profile.volt or cp.radarUnavailable) and
|
||||
(profile.topology != "gateway" or not cp.radarUnavailable) and
|
||||
(profile.topology not in ("ascm", "sdgm") or profile.radar != cp.radarUnavailable) and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and int(cp.flags) & ~int(GMFlags.HAS_BSM) == flags and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and int(cp.flags) & _GM_CONTROL_FLAGS_MASK == flags and
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
bool(cp.openpilotLongitudinalControl) == profile.longitudinal and
|
||||
bool(cp.pcmCruise) == (not profile.longitudinal and profile.topology == "camera") and
|
||||
@@ -1127,12 +1160,14 @@ def is_conventional_cc_pedal_profile(cp):
|
||||
if is_silverado_cc_pedal_profile(cp):
|
||||
return True
|
||||
try:
|
||||
if cp.brand != 'gm' or cp.carFingerprint not in ORDINARY_CC_CAR:
|
||||
return False
|
||||
flags = control_flags(cp)
|
||||
removed = bool(flags & GMFlags.NO_CAMERA)
|
||||
return (cp.brand == 'gm' and cp.carFingerprint in ORDINARY_CC_CAR and
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.radarUnavailable and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and
|
||||
bool(flags & GMFlags.PEDAL_LONG) and not flags & ~int(GMFlags.PEDAL_LONG | GMFlags.NO_CAMERA | GMFlags.NO_ACCELERATOR_POS_MSG) and
|
||||
bool(flags & _GM_PEDAL_LONG_FLAG) and not flags & ~_CC_PEDAL_ALLOWED_FLAGS and
|
||||
not cp.pcmCruise and len(cp.safetyConfigs) == 1 and
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
int(cp.safetyConfigs[0].safetyParam) ==
|
||||
@@ -1147,13 +1182,15 @@ SILVERADO_CC_PEDAL_WORDS = ((0xC182, 0xC183), (0xC184, 0xC185))
|
||||
def is_silverado_cc_pedal_profile(cp):
|
||||
"""Exact Silverado interceptor owner, including its distinct stock cancellation."""
|
||||
try:
|
||||
if cp.brand != 'gm' or cp.carFingerprint != CAR.CHEVROLET_SILVERADO_CC:
|
||||
return False
|
||||
flags = control_flags(cp)
|
||||
removed = bool(flags & GMFlags.NO_CAMERA)
|
||||
word = SILVERADO_CC_PEDAL_WORDS[not cp.openpilotLongitudinalControl][removed]
|
||||
return (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_SILVERADO_CC and
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.radarUnavailable and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and
|
||||
bool(flags & GMFlags.PEDAL_LONG) and not flags & ~int(GMFlags.PEDAL_LONG | GMFlags.NO_CAMERA | GMFlags.NO_ACCELERATOR_POS_MSG) and
|
||||
bool(flags & _GM_PEDAL_LONG_FLAG) and not flags & ~_CC_PEDAL_ALLOWED_FLAGS and
|
||||
not cp.pcmCruise and not cp.alphaLongitudinalAvailable and len(cp.safetyConfigs) == 1 and
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and int(cp.safetyConfigs[0].safetyParam) == word)
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
|
||||
@@ -70,11 +70,16 @@ def temporary_restriction(cp, state):
|
||||
(state.gearShifter not in (gears.drive, gears.sport, gears.low, gears.brake) or state.steerFaultTemporary))
|
||||
|
||||
|
||||
def native_observation(native, *, latched, panda_ready, restricted, now_ns, pending_since_ns):
|
||||
def native_observation(native, *, latched, panda_ready, restricted, now_ns, pending_since_ns,
|
||||
previously_acknowledged=False, host_requested_lateral=False):
|
||||
lost = not panda_ready or native is None
|
||||
if not latched or lost or restricted:
|
||||
return 0, lost, False
|
||||
if native.requestedLateral and native.lateralAllowed:
|
||||
return 0, False, False
|
||||
if (previously_acknowledged and host_requested_lateral and
|
||||
not native.requestedLateral and not native.lateralAllowed):
|
||||
# A current host request lost an established grant, not a cold handshake.
|
||||
return 0, False, True
|
||||
pending_since_ns = pending_since_ns or now_ns
|
||||
return pending_since_ns, False, now_ns - pending_since_ns >= 200_000_000
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.honda.interface import CarInterface
|
||||
from opendbc.car.honda.stock_aol import CLASSIC_BOSCH, NIDEC, RADARLESS, qualified, temporary_restriction
|
||||
from opendbc.car.honda.stock_aol import CLASSIC_BOSCH, NIDEC, RADARLESS, qualified, temporary_restriction, native_observation
|
||||
from opendbc.car.honda.values import CAR, HondaFlags
|
||||
from openpilot.starpilot.aol.intent import AolSettings, AOL_TOGGLE
|
||||
from openpilot.starpilot.car.honda.aol import policy_for, create_intent, native_accepts_cp
|
||||
@@ -347,6 +347,37 @@ def test_actual_card_active_observation_requires_fresh_applied_command(monkeypat
|
||||
assert tick(previous=True, long=True)["standard_active"]
|
||||
assert intent.allowed_latch # Actual normal long activity also proves active engagement.
|
||||
|
||||
# The actual Card caller consumes the acknowledged native permission. Its
|
||||
# immediate rejection boundary remains strict; no host grace is extended.
|
||||
tick(enabled=False)
|
||||
cs = state(True)
|
||||
native = replace(native, requestedLateral=True, lateralAllowed=True,
|
||||
observedMonoTime=now + 10_000_000, validUntilMonoTime=now + 210_000_000)
|
||||
tick(enabled=False)
|
||||
assert not intent.allowed_latch # The previously active latch toggles OFF.
|
||||
cs = state(False)
|
||||
native = replace(native, observedMonoTime=now + 10_000_000)
|
||||
tick(enabled=False)
|
||||
cs = state(True)
|
||||
native = replace(native, observedMonoTime=now + 10_000_000)
|
||||
tick(enabled=False)
|
||||
assert intent.allowed_latch # Release then a new physical press toggles ON.
|
||||
cs = state(False)
|
||||
native = replace(native, observedMonoTime=now + 10_000_000)
|
||||
assert tick(enabled=False)["native_rejection_ns"] == 0
|
||||
assert intent.allowed_latch
|
||||
native = replace(native, lateralAllowed=False, observedMonoTime=now + 10_000_000)
|
||||
observed_native = native.observedMonoTime
|
||||
assert tick(enabled=False)["native_rejection_ns"] == observed_native
|
||||
assert not intent.allowed_latch # Native denial immediately revokes independent intent.
|
||||
native = replace(native, lateralAllowed=True, observedMonoTime=now + 10_000_000)
|
||||
assert tick(enabled=False)["native_rejection_ns"] == 0
|
||||
assert not intent.allowed_latch # Permission recovery alone cannot recreate a gesture.
|
||||
cs = state(True)
|
||||
native = replace(native, observedMonoTime=now + 10_000_000)
|
||||
tick(enabled=False)
|
||||
assert intent.allowed_latch # A genuinely new LKAS edge can arm again.
|
||||
|
||||
generic = AolCardIntent(intent.settings)
|
||||
host.aol_card_intent = generic
|
||||
|
||||
@@ -357,3 +388,97 @@ def test_actual_card_active_observation_requires_fresh_applied_command(monkeypat
|
||||
monkeypatch.setattr(generic, "update", generic_boundary)
|
||||
with pytest.raises(BoundaryReached):
|
||||
host.state_update()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("acknowledged,requested,reset", ((False, True, False), (True, True, True),
|
||||
(True, False, False), (False, False, False)))
|
||||
def test_native_cold_pending_vs_requested_ack_loss(acknowledged, requested, reset):
|
||||
native = SimpleNamespace(requestedLateral=False, lateralAllowed=False)
|
||||
result = native_observation(native, latched=True, panda_ready=True, restricted=False,
|
||||
now_ns=1_000_000_000, pending_since_ns=0,
|
||||
previously_acknowledged=acknowledged, host_requested_lateral=requested)
|
||||
assert result == ((0, False, True) if reset else (1_000_000_000, False, False))
|
||||
if not reset:
|
||||
assert native_observation(native, latched=True, panda_ready=True, restricted=False,
|
||||
now_ns=1_200_000_000, pending_since_ns=result[0],
|
||||
previously_acknowledged=acknowledged, host_requested_lateral=requested) == (result[0], False, True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("changed", ("cold", "session", "panda", "cp", "manual_off", "restriction",
|
||||
"software_pause", "host_withdrawal", "invalid_axis", "stale_axis",
|
||||
"replaced_producer", "older_native", "resuming_pause", "pending_pause", "requested_denial", "unpaired_event",
|
||||
"fault_event", "invalid_event", "stale_event", "none"))
|
||||
def test_actual_card_ack_loss_requires_current_requested_same_identity(changed):
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
|
||||
from openpilot.starpilot.aol.wire import SafetyState
|
||||
|
||||
cp = final_cp(CAR.HONDA_CIVIC_2022, alpha=True, hybrid=True)
|
||||
cp.alternativeExperience |= 32
|
||||
now = 2_000_000_000
|
||||
native = SafetyState(1, True, now, now + 200_000_000, int(cp.safetyConfigs[0].safetyModel.raw),
|
||||
cp.safetyConfigs[0].safetyParam, True, False, True, False, "panda", "axis")
|
||||
axis = SimpleNamespace(qualified=True, sessionId="axis", observedMonoTime=now, validUntilMonoTime=now+30_000_000,
|
||||
faultSessionId="axis", faultReason="none", faultEventMonoTime=now, sequence=1,
|
||||
nativeAcknowledged=True, desiredLateral=True, lateralActive=True, sourceCarStateMonoTime=now,
|
||||
intentProducerSessionId="producer")
|
||||
|
||||
class Samples(dict):
|
||||
pass
|
||||
sm = Samples(aolAxisState=axis, onroadEvents=[])
|
||||
sm.valid = sm.alive = sm.seen = {"aolAxisState": True, "onroadEvents": True}
|
||||
sm.logMonoTime = {"aolAxisState": now, "onroadEvents": now}
|
||||
host = Car.__new__(Car)
|
||||
host.CP, host.sm, host.slc_producer_session = cp, sm, "producer"
|
||||
host.aol_transport_feedback = TransportPauseFeedback(cp)
|
||||
kwargs = dict(latched=True, panda_ready=True, restricted=False, now_ns=now, pending_since_ns=0)
|
||||
assert host.aol_honda_native_observation(native, **kwargs) == (0, False, False)
|
||||
if changed == "cold":
|
||||
host._honda_aol_acknowledgment = None
|
||||
elif changed == "session":
|
||||
native = replace(native, axisSessionId="replacement")
|
||||
elif changed == "panda":
|
||||
native = replace(native, pandaSerial="replacement")
|
||||
elif changed == "cp":
|
||||
host.CP = final_cp(CAR.HONDA_CIVIC_2022, alpha=False, hybrid=True)
|
||||
elif changed == "manual_off":
|
||||
kwargs['latched'] = False
|
||||
elif changed == "restriction":
|
||||
kwargs['restricted'] = True
|
||||
elif changed == "software_pause":
|
||||
axis.faultReason = "availabilityPause"
|
||||
host.aol_transport_feedback.retention_qualified = True
|
||||
elif changed == "host_withdrawal":
|
||||
axis.desiredLateral = False
|
||||
elif changed == "invalid_axis":
|
||||
sm.valid = {"aolAxisState": False}
|
||||
elif changed == "stale_axis":
|
||||
kwargs['now_ns'] += 40_000_000
|
||||
elif changed == "replaced_producer":
|
||||
axis.intentProducerSessionId = "replacement"
|
||||
elif changed == "resuming_pause":
|
||||
axis.lateralActive = False
|
||||
elif changed == "pending_pause":
|
||||
host.aol_transport_feedback.awaiting = True
|
||||
elif changed == "unpaired_event":
|
||||
axis.faultEventMonoTime -= 1
|
||||
elif changed == "fault_event":
|
||||
sm["onroadEvents"] = [SimpleNamespace(immediateDisable=True, softDisable=False)]
|
||||
elif changed == "invalid_event":
|
||||
sm.valid = {"aolAxisState": True, "onroadEvents": False}
|
||||
elif changed == "stale_event":
|
||||
axis.faultEventMonoTime = sm.logMonoTime["onroadEvents"] = now-1_500_000_001
|
||||
elif changed == "older_native":
|
||||
native = replace(native, observedMonoTime=now-1)
|
||||
withdrawn = replace(native, requestedLateral=False, lateralAllowed=False)
|
||||
if changed == "requested_denial":
|
||||
withdrawn = replace(withdrawn, requestedLateral=True)
|
||||
result = host.aol_honda_native_observation(withdrawn, **kwargs)
|
||||
if changed == "none":
|
||||
assert result == (0, False, True)
|
||||
else:
|
||||
assert not result[2] # No unexpected-loss cause is inferred from ambiguous zero.
|
||||
if changed in ("manual_off", "restriction"):
|
||||
assert result == (0, False, False)
|
||||
if changed in ("cold", "session", "panda", "cp", "manual_off"):
|
||||
assert host._honda_aol_acknowledgment is None
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Selected safety slot for the exact first-generation captured CANFD owner."""
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
||||
|
||||
|
||||
def topology_index(cp):
|
||||
if len(cp.safetyConfigs) == 1:
|
||||
return 0 if cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.hyundaiCanfd else None
|
||||
if len(cp.safetyConfigs) != 2:
|
||||
return None
|
||||
auxiliary, owner = cp.safetyConfigs
|
||||
required = HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG
|
||||
excluded = (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT |
|
||||
HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CCNC | HyundaiFlags.HYBRID)
|
||||
if (cp.brand != 'hyundai' or cp.carFingerprint not in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) or
|
||||
cp.passive or cp.dashcamOnly or cp.notCar or cp.steerControlType != CarParams.SteerControlType.torque or
|
||||
cp.flags & required != required or cp.flags & excluded or
|
||||
auxiliary.safetyModel != CarParams.SafetyModel.noOutput or auxiliary.safetyParam != 0 or
|
||||
owner.safetyModel != CarParams.SafetyModel.hyundaiCanfd):
|
||||
return None
|
||||
return 1
|
||||
|
||||
|
||||
def config_index(cp):
|
||||
owner = topology_index(cp)
|
||||
if owner != 1:
|
||||
return owner
|
||||
raw = int(cp.safetyConfigs[owner].safetyParam)
|
||||
if cp.openpilotLongitudinalControl and not cp.pcmCruise:
|
||||
if raw == 0x15 and cp.alternativeExperience == 0:
|
||||
return 1
|
||||
if cp.carFingerprint == CAR.KIA_EV6 and raw == 0x815 and cp.alternativeExperience == 32:
|
||||
return 1
|
||||
elif not cp.openpilotLongitudinalControl and cp.pcmCruise and cp.alternativeExperience == 0 and raw in (0x11, 0x811):
|
||||
return 1
|
||||
return None
|
||||
@@ -3,6 +3,7 @@
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.canfd_owner import config_index
|
||||
|
||||
STOCK_AOL_MARKER = 0x0800
|
||||
STOCK_EV_CARS = frozenset(
|
||||
@@ -27,22 +28,24 @@ STOCK_AOL_WORDS = frozenset(
|
||||
and (not extra & 0x2000 or (topology in (32, 40, 48, 176) and gas != 1))
|
||||
)
|
||||
|
||||
STOCK_AOL_FLAGS = (
|
||||
HyundaiFlags.CANFD
|
||||
| HyundaiFlags.EV
|
||||
| HyundaiFlags.HYBRID
|
||||
| HyundaiFlags.CANFD_LKA_STEER_MSG
|
||||
| HyundaiFlags.CANFD_LKA_STEER_MSG_ALT
|
||||
| HyundaiFlags.CANFD_ALT_BUTTONS
|
||||
| HyundaiFlags.CANFD_CAMERA_SCC
|
||||
| HyundaiFlags.CANFD_ALT_GEARS
|
||||
| HyundaiFlags.CANFD_ALT_GEARS_2
|
||||
| HyundaiFlags.CANFD_RADAR_SCC
|
||||
| HyundaiFlags.CANFD_NO_RADAR_DISABLE
|
||||
| HyundaiFlags.CCNC
|
||||
)
|
||||
|
||||
def _base_word(cp, *, experiences=(0,)):
|
||||
allowed = (
|
||||
HyundaiFlags.CANFD
|
||||
| HyundaiFlags.EV
|
||||
| HyundaiFlags.HYBRID
|
||||
| HyundaiFlags.CANFD_LKA_STEER_MSG
|
||||
| HyundaiFlags.CANFD_LKA_STEER_MSG_ALT
|
||||
| HyundaiFlags.CANFD_ALT_BUTTONS
|
||||
| HyundaiFlags.CANFD_CAMERA_SCC
|
||||
| HyundaiFlags.CANFD_ALT_GEARS
|
||||
| HyundaiFlags.CANFD_ALT_GEARS_2
|
||||
| HyundaiFlags.CANFD_RADAR_SCC
|
||||
| HyundaiFlags.CANFD_NO_RADAR_DISABLE
|
||||
| HyundaiFlags.CCNC
|
||||
)
|
||||
owner = config_index(cp)
|
||||
allowed = STOCK_AOL_FLAGS
|
||||
try:
|
||||
factory = CAR(cp.carFingerprint)
|
||||
except ValueError:
|
||||
@@ -59,8 +62,7 @@ def _base_word(cp, *, experiences=(0,)):
|
||||
or cp.steerControlType != CarParams.SteerControlType.torque
|
||||
or not cp.flags & HyundaiFlags.CANFD
|
||||
or int(cp.flags) & ~int(allowed)
|
||||
or len(cp.safetyConfigs) != 1
|
||||
or cp.safetyConfigs[0].safetyModel != CarParams.SafetyModel.hyundaiCanfd
|
||||
or owner is None
|
||||
):
|
||||
return None
|
||||
# EV identity is static; hybrid fuel is selected by the actual 0xFA fingerprint.
|
||||
@@ -74,7 +76,8 @@ def _base_word(cp, *, experiences=(0,)):
|
||||
if (ev and hybrid) or (alt_lka and not lka) or (lka and camera):
|
||||
return None
|
||||
bus = CanBus(cp)
|
||||
if (bus.ACAN, bus.ECAN, bus.CAM) != ((0, 1, 2) if lka else (1, 0, 2)):
|
||||
offset = 4 * owner
|
||||
if (bus.ACAN - offset, bus.ECAN - offset, bus.CAM - offset) != ((0, 1, 2) if lka else (1, 0, 2)):
|
||||
return None
|
||||
word = (1 if ev else 2 if hybrid else 0) | (16 if lka else 0) | (128 if alt_lka else 0)
|
||||
word |= (32 if alt_buttons else 0) | (8 if camera else 0)
|
||||
@@ -89,7 +92,7 @@ def qualified(cp, *, marked_only=False):
|
||||
word = _base_word(cp)
|
||||
if word is None or cp.openpilotLongitudinalControl or not cp.pcmCruise:
|
||||
return False
|
||||
return cp.safetyConfigs[0].safetyParam in ((word | STOCK_AOL_MARKER,) if marked_only else (word, word | STOCK_AOL_MARKER))
|
||||
return cp.safetyConfigs[config_index(cp)].safetyParam in ((word | STOCK_AOL_MARKER,) if marked_only else (word, word | STOCK_AOL_MARKER))
|
||||
|
||||
|
||||
LONG_AOL_WORDS = frozenset((0x0815, 0x0895))
|
||||
|
||||
@@ -6,21 +6,30 @@ from opendbc.can.parser import MAX_BAD_COUNTER
|
||||
from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus
|
||||
from opendbc.car.hyundai.values import HyundaiFlags
|
||||
from opendbc.car.hyundai.canfd_owner import topology_index
|
||||
|
||||
|
||||
def first_generation(cp):
|
||||
owner = topology_index(cp)
|
||||
if owner is None:
|
||||
return False
|
||||
buses = CanBus(cp)
|
||||
offset = 4 * owner
|
||||
excluded = (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT |
|
||||
HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CCNC |
|
||||
HyundaiFlags.HYBRID)
|
||||
return (cp.flags & HyundaiFlags.CANFD and
|
||||
cp.flags & HyundaiFlags.EV and cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG and
|
||||
not cp.flags & excluded and CanBus(cp).ACAN == 0 and CanBus(cp).ECAN == 1 and
|
||||
not cp.flags & excluded and (buses.ACAN, buses.ECAN, buses.CAM) == (offset, offset + 1, offset + 2) and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar)
|
||||
|
||||
|
||||
class CapturedADRVStartup(HyundaiECUStartup):
|
||||
def __init__(self, cp, callbacks, *, label, template_type):
|
||||
super().__init__(cp, callbacks, address=0x730, bus=1, label=label)
|
||||
buses = CanBus(cp)
|
||||
super().__init__(cp, callbacks, address=0x730, bus=buses.ECAN, label=label)
|
||||
self.template_bus = buses.ACAN
|
||||
self.camera_bus = buses.CAM
|
||||
self.template_type = template_type
|
||||
self.template = None
|
||||
|
||||
@@ -35,7 +44,7 @@ class CapturedADRVStartup(HyundaiECUStartup):
|
||||
if not floor < stamp <= now or now - stamp > 150_000_000:
|
||||
continue
|
||||
for msg in packet:
|
||||
if msg.src != 0 or msg.address != 0x51:
|
||||
if msg.src != self.template_bus or msg.address != 0x51:
|
||||
continue
|
||||
try:
|
||||
candidate = self.template_type.capture(bytes(msg.dat))
|
||||
@@ -66,7 +75,7 @@ class CapturedADRVStartup(HyundaiECUStartup):
|
||||
return False
|
||||
pt = ci.can_parsers[Bus.pt]
|
||||
cam = ci.can_parsers[Bus.cam]
|
||||
if pt.bus != 1 or cam.bus != 2:
|
||||
if pt.bus != self.bus or cam.bus != self.camera_bus:
|
||||
return False
|
||||
names = ['ACCELERATOR', 'TCS', 'WHEEL_SPEEDS', 'MDPS', 'CRUISE_BUTTONS', ci.CS.gear_msg_canfd]
|
||||
if not ci.CP.openpilotLongitudinalControl:
|
||||
|
||||
@@ -163,6 +163,8 @@ class CarController(CarControllerBase):
|
||||
self._ray_prev_lat_active = CC.latActive
|
||||
|
||||
# steering torque
|
||||
if self.CP.flags & HyundaiFlags.CANFD and self.angle_vm is None:
|
||||
self.params = CarControllerParams(self.CP, CS.out.vEgoRaw)
|
||||
new_torque = int(round(actuators.torque * self.params.STEER_MAX))
|
||||
apply_torque = (0 if self.angle_vm is not None else
|
||||
apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.params))
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
"""Hyundai prepublication ECU transaction; sent disable is not confirmed ownership."""
|
||||
from enum import Enum
|
||||
from collections.abc import Callable
|
||||
from typing import TypedDict, cast
|
||||
from copy import deepcopy
|
||||
import time
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.disable_ecu import disable_ecu
|
||||
from opendbc.car.hyundai.values import HyundaiSafetyFlags
|
||||
from opendbc.car.hyundai.canfd_owner import config_index
|
||||
from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
|
||||
|
||||
|
||||
class PreparedSafetyConfig(TypedDict):
|
||||
safetyParam: int
|
||||
|
||||
|
||||
class PreparedParams(TypedDict):
|
||||
safetyConfigs: list[PreparedSafetyConfig]
|
||||
alternativeExperience: int
|
||||
|
||||
|
||||
class Outcome(Enum):
|
||||
STOCK_UNTOUCHED = 'stock_untouched'
|
||||
SENT_UNCONFIRMED = 'sent_unconfirmed'
|
||||
@@ -60,7 +72,8 @@ class HyundaiECUStartup:
|
||||
self.callbacks[1](frames)
|
||||
|
||||
def _restore(self):
|
||||
if not self.admission():
|
||||
admission = cast(Callable[[], bool], self.admission)
|
||||
if not admission():
|
||||
return False
|
||||
recv, send = self.callbacks
|
||||
session = self._query(send, recv, self.bus, [(self.address, None)], [b'\x10\x03'], [b'\x50\x03'])
|
||||
@@ -126,11 +139,12 @@ class HyundaiECUStartup:
|
||||
return
|
||||
from opendbc.car.hyundai.ev6_aol import qualified as ev6_aol_qualified
|
||||
if self.outcome is Outcome.SENT_UNCONFIRMED and ev6_aol_qualified(ci.CP, marked_only=True):
|
||||
expected = deepcopy(self.prepared_cp)
|
||||
expected = deepcopy(cast(PreparedParams, self.prepared_cp))
|
||||
configs = expected.get('safetyConfigs', [])
|
||||
if len(configs) != 1 or configs[0]['safetyParam'] != 0x15 or expected['alternativeExperience'] != 0:
|
||||
owner = config_index(ci.CP)
|
||||
if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] != 0x15 or expected['alternativeExperience'] != 0:
|
||||
raise RuntimeError('EV6 prepared LONG profile cannot be finalized')
|
||||
configs[0]['safetyParam'] |= 0x0800
|
||||
configs[owner]['safetyParam'] |= 0x0800
|
||||
expected['alternativeExperience'] = 32
|
||||
if ci.CP.to_dict() != expected:
|
||||
raise RuntimeError('EV6 AOL finalization changed unrelated CarParams')
|
||||
@@ -140,11 +154,12 @@ class HyundaiECUStartup:
|
||||
if (self.outcome not in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) or
|
||||
not stock_aol_qualified(ci.CP, marked_only=True)):
|
||||
raise RuntimeError('Hyundai startup does not admit this AOL configuration')
|
||||
expected = deepcopy(self.prepared_cp)
|
||||
expected = deepcopy(cast(PreparedParams, self.prepared_cp))
|
||||
configs = expected.get('safetyConfigs', [])
|
||||
if len(configs) != 1 or configs[0]['safetyParam'] not in (0x11, 0x91):
|
||||
owner = config_index(ci.CP)
|
||||
if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] not in (0x11, 0x91):
|
||||
raise RuntimeError('Hyundai startup stock profile cannot be finalized')
|
||||
configs[0]['safetyParam'] |= 0x0800
|
||||
configs[owner]['safetyParam'] |= 0x0800
|
||||
if ci.CP.to_dict() != expected:
|
||||
raise RuntimeError('Hyundai AOL finalization changed unrelated CarParams')
|
||||
self.prepared_cp = expected
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""First-generation EV6 physical authorization for the existing torque lease."""
|
||||
from opendbc.car.hyundai.canfd_stock_aol import _base_word
|
||||
from opendbc.car.hyundai.canfd_owner import config_index
|
||||
from opendbc.car.hyundai.ev6_startup import eligible
|
||||
from opendbc.car.hyundai.values import Buttons
|
||||
|
||||
@@ -9,7 +10,7 @@ def qualified(cp, *, marked_only=False):
|
||||
return False
|
||||
if _base_word(cp, experiences=(0, 32)) != 0x11:
|
||||
return False
|
||||
word = cp.safetyConfigs[0].safetyParam
|
||||
word = cp.safetyConfigs[config_index(cp)].safetyParam
|
||||
return (word == 0x815 and cp.alternativeExperience == 32) or (
|
||||
not marked_only and word == 0x15 and cp.alternativeExperience == 0)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
import numpy as np
|
||||
from opendbc.car import CanBusBase, CanData
|
||||
@@ -213,8 +214,8 @@ def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_ov
|
||||
object_distance, object_relative, object_valid, object_status = 1., 0., 0, 2
|
||||
else:
|
||||
visible = bool(lead_visible)
|
||||
object_distance = float(np.clip(lead_distance if visible else 0., 0., 204.7))
|
||||
object_relative = float(np.clip(lead_rel_speed if visible else 0., -16.4, 34.7))
|
||||
object_distance = float(np.clip(cast(float, lead_distance) if visible else 0., 0., 204.7))
|
||||
object_relative = float(np.clip(cast(float, lead_rel_speed) if visible else 0., -16.4, 34.7))
|
||||
object_valid = int(not visible)
|
||||
object_status = 0 if not (enabled and visible) else 1 if gas_override else 2
|
||||
|
||||
@@ -392,11 +393,15 @@ def create_adrv_messages(packer, CAN, frame, *, template=None, drive_gear=False,
|
||||
values = {
|
||||
}
|
||||
from opendbc.car.hyundai.gv70_template import GV70Template
|
||||
if isinstance(template, GV70Template):
|
||||
ret.append(template.frame(frame, drive_gear, CAN.ACAN, speed=speed))
|
||||
if template is not None:
|
||||
if type(CAN.ACAN) is not int or type(CAN.offset) is not int:
|
||||
raise ValueError('Captured ADRV requires integer CAN routing')
|
||||
local_bus = CAN.ACAN - CAN.offset
|
||||
packet = (template.frame(frame, drive_gear, local_bus, speed=speed) if isinstance(template, GV70Template) else
|
||||
template.frame(frame, drive_gear, local_bus))
|
||||
ret.append(CanData(packet.address, packet.dat, CAN.ACAN))
|
||||
else:
|
||||
ret.append(template.frame(frame, drive_gear, CAN.ACAN) if template is not None else
|
||||
packer.make_can_msg("ADRV_0x51", CAN.ACAN, values))
|
||||
ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, values))
|
||||
|
||||
ret.extend(create_fca_warning_light(packer, CAN, frame))
|
||||
|
||||
|
||||
@@ -150,6 +150,8 @@ class CarInterface(CarInterfaceBase):
|
||||
else:
|
||||
ret.flags |= HyundaiFlags.CANFD_ALT_GEARS.value
|
||||
|
||||
if candidate in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and CAN.offset >= 8:
|
||||
ret.dashcamOnly = True
|
||||
cfgs = [get_safety_config(structs.CarParams.SafetyModel.hyundaiCanfd), ]
|
||||
if CAN.ECAN >= 4:
|
||||
cfgs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput))
|
||||
|
||||
@@ -237,8 +237,8 @@ class TestBlendedLifecycle(unittest.TestCase):
|
||||
calls.append('ack')
|
||||
context.owner = SimpleNamespace(sent=sent)
|
||||
car = card.Car.__new__(card.Car)
|
||||
car.vehicle_startup = context
|
||||
car.pm = SimpleNamespace(send=lambda *args: calls.append('published'))
|
||||
vars(car)['vehicle_startup'] = context
|
||||
vars(car)['pm'] = SimpleNamespace(send=lambda *args: calls.append('published'))
|
||||
card.Car.publish_sendcan(car, [], valid=True)
|
||||
self.assertEqual(calls, ['published', 'ack'])
|
||||
|
||||
@@ -320,17 +320,23 @@ class TestBlendedLifecycle(unittest.TestCase):
|
||||
calls = []
|
||||
CS = SimpleNamespace()
|
||||
car = card.Car.__new__(card.Car)
|
||||
car.state_update = lambda: (CS, None)
|
||||
car.state_publish = lambda *args: calls.append('state')
|
||||
|
||||
def state_update(*, defer_radar=False):
|
||||
self.assertTrue(defer_radar)
|
||||
return CS, None
|
||||
vars(car)['state_update'] = state_update
|
||||
car._deferred_radar_can = None
|
||||
vars(car)['RI'] = SimpleNamespace(update=lambda can_list: None)
|
||||
vars(car)['state_publish'] = lambda *args: calls.append('state')
|
||||
car.CP = SimpleNamespace(passive=False)
|
||||
car.ci_initialized = False
|
||||
|
||||
class SM(dict):
|
||||
seen = {'onroadEvents': False}
|
||||
car.sm = SM(onroadEvents=[])
|
||||
car.vehicle_startup = SimpleNamespace(owner=object(), maintain=lambda **kw: calls.append(('maintain', kw)))
|
||||
car.startup_panda_configured = lambda: True
|
||||
car.controls_update = lambda *args: self.fail('Controls are not initialized')
|
||||
vars(car)['sm'] = SM(onroadEvents=[])
|
||||
vars(car)['vehicle_startup'] = SimpleNamespace(owner=object(), maintain=lambda **kw: calls.append(('maintain', kw)))
|
||||
vars(car)['startup_panda_configured'] = lambda: True
|
||||
vars(car)['controls_update'] = lambda *args: self.fail('Controls are not initialized')
|
||||
card.Car.step(car)
|
||||
self.assertEqual(calls, [('maintain', {'configured': True}), 'state'])
|
||||
self.assertIs(car.CS_prev, CS)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Captured ADRV encoding keeps local validation and Panda routing separate."""
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, CanData, structs
|
||||
from opendbc.car.hyundai.ev6_template import EV6Template
|
||||
from opendbc.car.hyundai.gv70_template import GV70Template
|
||||
from opendbc.car.hyundai.hyundaicanfd import CanBus, create_adrv_messages, hkg_can_fd_checksum
|
||||
from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags
|
||||
|
||||
CAPTURE = bytes.fromhex('88ed2e091700ffff5e0d0000012006ff021c2200000000000800000010000000')
|
||||
|
||||
|
||||
def buses(pandas: int, *, lka: bool = True) -> CanBus:
|
||||
configs = [structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.noOutput) for _ in range(pandas - 1)]
|
||||
configs.append(structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.hyundaiCanfd))
|
||||
cp = structs.CarParams(safetyConfigs=configs, flags=int(HyundaiFlags.CANFD_LKA_STEER_MSG) if lka else 0)
|
||||
return CanBus(cp)
|
||||
|
||||
|
||||
class TestCapturedADRVRouting(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.packer = CANPacker(DBC[CAR.KIA_EV6][Bus.pt])
|
||||
|
||||
def test_ev6_and_gv70_use_local_encoding_and_logical_owner_bus(self):
|
||||
for pandas in (1, 2):
|
||||
can = buses(pandas)
|
||||
self.assertEqual((can.offset, can.ACAN, can.ECAN), (4 * (pandas - 1), 4 * (pandas - 1), 4 * (pandas - 1) + 1))
|
||||
for template in (EV6Template.capture(CAPTURE), GV70Template.capture(CAPTURE)):
|
||||
for frame in (0, 7, 255, 256):
|
||||
for drive in (False, True):
|
||||
for speed in (None, 12.345):
|
||||
with self.subTest(pandas=pandas, template=type(template).__name__, frame=frame, drive=drive, speed=speed):
|
||||
local = (template.frame(frame, drive, 0, speed=speed) if isinstance(template, GV70Template) else
|
||||
template.frame(frame, drive, 0))
|
||||
sent = create_adrv_messages(self.packer, can, frame, template=template, drive_gear=drive, speed=speed)
|
||||
self.assertEqual(sent[0], CanData(local.address, local.dat, can.ACAN))
|
||||
self.assertEqual(int.from_bytes(sent[0].dat[:2], 'little'), hkg_can_fd_checksum(0x51, None, bytearray(sent[0].dat)))
|
||||
self.assertEqual(template.data, CAPTURE)
|
||||
self.assertTrue(all(packet[2] == can.ECAN for packet in sent[1:]))
|
||||
if pandas == 2:
|
||||
self.assertNotEqual(sent[0].src, 0)
|
||||
|
||||
def test_default_packer_sender_preserves_logical_bus_for_both_harnesses(self):
|
||||
expected_packer = CANPacker(DBC[CAR.KIA_EV6][Bus.pt])
|
||||
for pandas in (1, 2):
|
||||
for lka in (False, True):
|
||||
can = buses(pandas, lka=lka)
|
||||
expected = expected_packer.make_can_msg('ADRV_0x51', can.ACAN, {})
|
||||
self.assertEqual(create_adrv_messages(self.packer, can, 7)[0], expected)
|
||||
|
||||
def test_wrong_local_bus_and_noninteger_routing_remain_rejected(self):
|
||||
for pandas in (1, 2):
|
||||
for template in (EV6Template.capture(CAPTURE), GV70Template.capture(CAPTURE)):
|
||||
with self.subTest(pandas=pandas, template=type(template).__name__, local_bus=1):
|
||||
with self.assertRaises(ValueError):
|
||||
create_adrv_messages(self.packer, buses(pandas, lka=False), 7, template=template, drive_gear=True)
|
||||
for field, value in (('_a', False), ('_a', 0.0), ('offset', False), ('offset', 0.0)):
|
||||
can = buses(pandas)
|
||||
setattr(can, field, value)
|
||||
with self.subTest(pandas=pandas, template=type(template).__name__, field=field, value=value):
|
||||
with self.assertRaises(ValueError):
|
||||
create_adrv_messages(self.packer, can, 7, template=template, drive_gear=True)
|
||||
|
||||
def test_template_physical_bus_and_drive_guards_remain_closed(self):
|
||||
for pandas in (1, 2):
|
||||
for template in (EV6Template.capture(CAPTURE), GV70Template.capture(CAPTURE)):
|
||||
for drive in (0, 1):
|
||||
with self.assertRaises(ValueError):
|
||||
create_adrv_messages(self.packer, buses(pandas), 7, template=template, drive_gear=drive)
|
||||
for bus in (False, 1, 4):
|
||||
with self.assertRaises(ValueError):
|
||||
template.frame(7, True, bus)
|
||||
@@ -579,8 +579,8 @@ class TestIoniq6PrepublicationHandoff(unittest.TestCase):
|
||||
card.ioniq6_handoff_result = None
|
||||
card.can_callbacks = (lambda wait_for_one=False: [], lambda messages: None)
|
||||
card.params = SimpleNamespace(put_bool=Mock())
|
||||
card.CP = SimpleNamespace(safetyConfigs=[object()])
|
||||
card.CI = SimpleNamespace(CC=SimpleNamespace(), init=Mock(), apply=Mock(return_value=(object(), [])))
|
||||
card.CP = SimpleNamespace(brand='hyundai', safetyConfigs=[object()])
|
||||
card.CI = SimpleNamespace(CC=SimpleNamespace(), CS=SimpleNamespace(), init=Mock(), apply=Mock(return_value=(object(), [])))
|
||||
card.vehicle_startup = VehicleStartupOwner()
|
||||
card.sm = FakeSM()
|
||||
card.publish_sendcan = Mock()
|
||||
|
||||
@@ -23,7 +23,7 @@ from openpilot.starpilot.car.hyundai.aol import policy_for
|
||||
|
||||
def card_publication_sequence(context):
|
||||
# Run the reached production statements, retaining their actual source order.
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.car.card import Car, hyundai_aol_safety_config
|
||||
source=textwrap.dedent(inspect.getsource(Car.__init__))
|
||||
tree=ast.parse(source)
|
||||
statements=[]
|
||||
@@ -40,7 +40,7 @@ def card_publication_sequence(context):
|
||||
calls=[statement.value.func.attr for statement in statements if isinstance(statement,ast.Expr)]
|
||||
assert calls==['configure','finalize_aol_configuration','seal_publication']
|
||||
exec(compile(ast.Module(body=statements,type_ignores=[]),'actual-card-publication-order','exec'),
|
||||
{'self':context,'aol_policy':policy_for(context.CP)})
|
||||
{'self':context,'aol_policy':policy_for(context.CP), 'hyundai_aol_safety_config':hyundai_aol_safety_config})
|
||||
|
||||
|
||||
@pytest.fixture(params=[(CAR.KIA_EV6,EV6Startup),(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,GV70Startup)])
|
||||
|
||||
@@ -17,7 +17,7 @@ class CarControllerParams:
|
||||
ACCEL_MAX = 2.0 # m/s^2
|
||||
ANGLE_LIMITS = AngleSteeringLimitsVM(360, MAX_ANGLE_RATE=5)
|
||||
|
||||
def __init__(self, CP):
|
||||
def __init__(self, CP, vEgoRaw=100.):
|
||||
self.ACCEL_MAX = 3.5 if is_blended_alpha(CP) else type(self).ACCEL_MAX
|
||||
self.STEER_DELTA_UP = 3
|
||||
self.STEER_DELTA_DOWN = 7
|
||||
@@ -28,12 +28,16 @@ class CarControllerParams:
|
||||
self.STEER_STEP = 1 # 100 Hz
|
||||
|
||||
if CP.flags & HyundaiFlags.CANFD:
|
||||
self.STEER_MAX = 270
|
||||
self.STEER_MAX = 270 if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING else 409
|
||||
self.STEER_DRIVER_ALLOWANCE = 250
|
||||
self.STEER_DRIVER_MULTIPLIER = 2
|
||||
self.STEER_THRESHOLD = 250
|
||||
self.STEER_DELTA_UP = 2
|
||||
self.STEER_DELTA_DOWN = 3
|
||||
if (not CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and 0 <= vEgoRaw < 15 and
|
||||
CP.carFingerprint != CAR.KIA_CARNIVAL_HEV_4TH_GEN):
|
||||
self.STEER_DELTA_UP = 10
|
||||
self.STEER_DELTA_DOWN = 8
|
||||
|
||||
# To determine the limit for your car, find the maximum value that the stock LKAS will request.
|
||||
# If the max stock LKAS request is <384, add your car to this list.
|
||||
|
||||
@@ -3,7 +3,8 @@ import unittest
|
||||
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.docs import get_all_car_docs
|
||||
from opendbc.car.docs_definitions import Cable, Column, PartType, Star, SupportType
|
||||
from opendbc.car.docs_definitions import Cable, CarHarness, Column, PartType, Star, SupportType
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.honda.values import CAR as HONDA
|
||||
from opendbc.car.values import PLATFORMS
|
||||
|
||||
@@ -63,8 +64,48 @@ class TestCarDocs(unittest.TestCase):
|
||||
if car.name == "comma body":
|
||||
raise unittest.SkipTest
|
||||
|
||||
if car.brand == "gm" and not car.merged and not car.years:
|
||||
# Community configurations may have no established model-year range.
|
||||
self.assertEqual(car.support_type, SupportType.COMMUNITY)
|
||||
self.assertEqual(car.year_list, [])
|
||||
continue
|
||||
|
||||
assert car.years and car.year_list, f"Format years correctly: {car.name}"
|
||||
|
||||
def test_volt_docs_support_and_hardware(self):
|
||||
expected = {
|
||||
GM.CHEVROLET_VOLT: (SupportType.COMMUNITY, CarHarness.obd_ii),
|
||||
GM.CHEVROLET_VOLT_ASCM: (SupportType.COMMUNITY, CarHarness.custom),
|
||||
GM.CHEVROLET_VOLT_2019: (SupportType.COMMUNITY, CarHarness.gmsdgm),
|
||||
GM.CHEVROLET_VOLT_CAMERA: (SupportType.COMMUNITY, CarHarness.gm),
|
||||
GM.CHEVROLET_VOLT_CC: (SupportType.COMMUNITY, CarHarness.custom),
|
||||
}
|
||||
docs = {car.car_fingerprint: car for car in self.all_cars if car.car_fingerprint in expected}
|
||||
self.assertEqual(set(docs), set(expected))
|
||||
for platform, (support, harness) in expected.items():
|
||||
with self.subTest(platform=platform):
|
||||
self.assertEqual(docs[platform].make, "Chevrolet")
|
||||
self.assertEqual(docs[platform].support_type, support)
|
||||
self.assertEqual([part for part in docs[platform].car_parts.parts if isinstance(part, CarHarness)], [harness])
|
||||
|
||||
def test_every_gm_platform_has_minimal_docs(self):
|
||||
from opendbc.car.gm.values import GMCommunityCarDocs
|
||||
|
||||
documented = {car.car_fingerprint for car in self.all_cars}
|
||||
for platform in GM:
|
||||
with self.subTest(platform=platform):
|
||||
self.assertIn(platform, documented)
|
||||
self.assertTrue(platform.config.car_docs)
|
||||
for doc in platform.config.car_docs:
|
||||
self.assertTrue(doc.make and doc.model and doc.package)
|
||||
if isinstance(doc, GMCommunityCarDocs):
|
||||
self.assertEqual(doc.support_type, SupportType.COMMUNITY)
|
||||
self.assertFalse(doc.merged)
|
||||
expected_harness = (CarHarness.custom if "ASCM Harness" in doc.name or "(CC)" in doc.name else
|
||||
CarHarness.gmsdgm if "SDGM Harness" in doc.name else
|
||||
CarHarness.gm if "Camera Harness" in doc.name else CarHarness.obd_ii)
|
||||
self.assertIn(expected_harness, doc.car_parts.parts)
|
||||
|
||||
def test_harnesses(self):
|
||||
for car in self.all_cars:
|
||||
with self.subTest(car=car.name):
|
||||
|
||||
@@ -377,7 +377,7 @@ class CarController(CarControllerBase):
|
||||
if self.frame % 20 == 0 or send_ui:
|
||||
can_sends.append(toyotacan.create_ui_command(self.packer, steer_alert, pcm_cancel_cmd, hud_control.leftLaneVisible,
|
||||
hud_control.rightLaneVisible, hud_control.leftLaneDepart,
|
||||
hud_control.rightLaneDepart, CC.enabled, CS.lkas_hud))
|
||||
hud_control.rightLaneDepart, lat_active, CS.lkas_hud))
|
||||
|
||||
if (self.frame % 100 == 0 or send_ui) and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
can_sends.append(toyotacan.create_fcw_command(self.packer, fcw_alert))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import unittest
|
||||
from typing import cast
|
||||
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.fw_query_definitions import OfflineFwVersions
|
||||
from opendbc.car.fw_versions import build_fw_dict
|
||||
from opendbc.car.toyota.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.toyota.values import CAR, DBC, ToyotaFlags, FW_QUERY_CONFIG, PLATFORM_CODE_ECUS, \
|
||||
@@ -21,6 +23,38 @@ def cars_with(flags):
|
||||
|
||||
|
||||
class TestToyotaInterfaces(unittest.TestCase):
|
||||
def test_dashboard_lanes_follow_applied_lateral_axis(self):
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.toyota.tests.test_auto_hold import setup_hold, step_hold, decode
|
||||
|
||||
for car in (CAR.TOYOTA_PRIUS_RETROFIT, CAR.TOYOTA_COROLLA_TSS2, CAR.TOYOTA_RAV4_TSS2):
|
||||
cp, controller, command, state = setup_hold(car, enabled=False)
|
||||
state.out.standstill = state.out.brakePressed = False
|
||||
command.actuators.torque = 0.2
|
||||
command.cruiseControl.cancel = True
|
||||
command.hudControl.visualAlert = structs.CarControl.HUDControl.VisualAlert.steerRequired
|
||||
command.hudControl.leftLaneVisible = True
|
||||
command.hudControl.rightLaneDepart = True
|
||||
state.lkas_hud = {'LANE_SWAY_FLD': 5, 'LANE_SWAY_BUZZER': 1, 'LANE_SWAY_WARNING': 1,
|
||||
'LANE_SWAY_SENSITIVITY': 1, 'LANE_SWAY_TOGGLE': 0}
|
||||
cases = ((True, False, 0, False), (False, True, 0, True), (True, True, 499, True),
|
||||
(True, True, 500, False), (True, True, -500, False), (True, True, 0, True),
|
||||
(True, False, 0, False))
|
||||
for enabled, requested, driver_torque, applied in cases:
|
||||
with self.subTest(car=car, enabled=enabled, requested=requested, driver_torque=driver_torque):
|
||||
command.enabled, command.latActive = enabled, requested
|
||||
state.out.steeringTorque = driver_torque
|
||||
frames = step_hold(controller, command, state)
|
||||
hud = decode(cp, next(frame for frame in frames if frame[0] == 0x412), 'LKAS_HUD')
|
||||
steering = decode(cp, next(frame for frame in frames if frame[0] == 0x2E4), 'STEERING_LKA')
|
||||
self.assertEqual((hud['LEFT_LINE'], hud['RIGHT_LINE'], hud['BARRIERS']),
|
||||
(1, 3, 1) if applied else (0, 0, 0))
|
||||
self.assertEqual(steering['STEER_REQUEST'], int(applied))
|
||||
self.assertEqual(steering['STEER_TORQUE_CMD'] != 0, applied)
|
||||
self.assertEqual((hud['LKAS_STATUS'], hud['LDA_SA_TOGGLE'], hud['LDA_ALERT'], hud['TWO_BEEPS']), (1, 1, 1, 1))
|
||||
for signal, value in state.lkas_hud.items():
|
||||
self.assertEqual(hud[signal], value)
|
||||
|
||||
def test_distance_command_matches_hud_bars_until_pcm_acknowledges(self):
|
||||
from opendbc.car.toyota.tests.test_auto_hold import setup_hold, step_hold, decode
|
||||
|
||||
@@ -210,6 +244,8 @@ class TestToyotaFingerprint(unittest.TestCase):
|
||||
|
||||
def test_fuzzy_excluded_platforms(self):
|
||||
# Asserts a list of platforms that will not fuzzy fingerprint with platform codes due to them being shared.
|
||||
matcher = FW_QUERY_CONFIG.match_fw_to_car_fuzzy
|
||||
assert matcher is not None
|
||||
platforms_with_shared_codes = set()
|
||||
for platform, fw_by_addr in FW_VERSIONS.items():
|
||||
car_fw = []
|
||||
@@ -220,7 +256,7 @@ class TestToyotaFingerprint(unittest.TestCase):
|
||||
subAddress=0 if sub_addr is None else sub_addr))
|
||||
|
||||
CP = CarParams(carFw=car_fw)
|
||||
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
|
||||
matches = matcher(build_fw_dict(CP.carFw), CP.carVin, cast(OfflineFwVersions, FW_VERSIONS))
|
||||
if len(matches) == 1:
|
||||
assert list(matches)[0] == platform
|
||||
else:
|
||||
|
||||
@@ -142,13 +142,13 @@ def create_fcw_command(packer, fcw):
|
||||
return packer.make_can_msg("PCS_HUD", 0, values)
|
||||
|
||||
|
||||
def create_ui_command(packer, steer, chime, left_line, right_line, left_lane_depart, right_lane_depart, enabled, stock_lkas_hud):
|
||||
def create_ui_command(packer, steer, chime, left_line, right_line, left_lane_depart, right_lane_depart, lat_active, stock_lkas_hud):
|
||||
values = {
|
||||
"TWO_BEEPS": chime,
|
||||
"LDA_ALERT": steer,
|
||||
"RIGHT_LINE": 3 if right_lane_depart else 1 if right_line else 2,
|
||||
"LEFT_LINE": 3 if left_lane_depart else 1 if left_line else 2,
|
||||
"BARRIERS": 1 if enabled else 0,
|
||||
"RIGHT_LINE": 0 if not lat_active else 3 if right_lane_depart else 1 if right_line else 2,
|
||||
"LEFT_LINE": 0 if not lat_active else 3 if left_lane_depart else 1 if left_line else 2,
|
||||
"BARRIERS": 1 if lat_active else 0,
|
||||
|
||||
# static signals
|
||||
"SET_ME_X02": 2,
|
||||
|
||||
@@ -8,15 +8,36 @@
|
||||
|
||||
bool ignition_can = false;
|
||||
uint32_t ignition_can_cnt = 0U;
|
||||
bool wake_on_can = false;
|
||||
uint32_t wake_on_can_cnt = 0U;
|
||||
#ifdef PANDA_HKG_REMOTE_START
|
||||
bool hkg_remote_climate_wake = false;
|
||||
uint32_t hkg_remote_climate_wake_cnt = 0U;
|
||||
#endif
|
||||
|
||||
void ignition_can_hook(const CANPacket_t *msg) {
|
||||
#ifdef PANDA_HKG_REMOTE_START
|
||||
if (msg_matches(msg, 0x384U, 1U, 8U)) {
|
||||
hkg_remote_climate_wake = msg->data[3] != 0U;
|
||||
hkg_remote_climate_wake_cnt = 0U;
|
||||
}
|
||||
#endif
|
||||
|
||||
// GM exception
|
||||
#ifdef PANDA_GM_REMOTE_START_C9
|
||||
if (msg_matches(msg, 0xC9U, 0U, 8U)) {
|
||||
ignition_can = (msg->data[6] & 0x10U) != 0U;
|
||||
ignition_can_cnt = 0U;
|
||||
}
|
||||
#else
|
||||
if (msg_matches(msg, 0x1F1U, 0U, 8U)) {
|
||||
// SystemPowerMode (2=Run, 3=Crank Request)
|
||||
ignition_can = (msg->data[0] & 0x2U) != 0U;
|
||||
ignition_can_cnt = 0U;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Rivian R1S/T GEN1 exception
|
||||
if (msg_matches(msg, 0x152U, 0U, 8U)) {
|
||||
// 0x152 overlaps with Subaru pre-global which has this bit as the high beam
|
||||
@@ -44,6 +65,38 @@ void ignition_can_hook(const CANPacket_t *msg) {
|
||||
ignition_can_cnt = 0U;
|
||||
}
|
||||
prev_counter_tesla = counter;
|
||||
#ifdef PANDA_TESLA_WAKE_ON_CAN
|
||||
uint32_t checksum = (msg->addr & 0xFFU) + (msg->addr >> 8U);
|
||||
for (uint8_t i = 0U; i < 7U; i++) {
|
||||
checksum += msg->data[i];
|
||||
}
|
||||
static int prev_counter_tesla_wake = -1;
|
||||
if (!msg->extended && (msg->data[7] == (checksum & 0xFFU))) {
|
||||
if ((prev_counter_tesla_wake != -1) && (counter == ((prev_counter_tesla_wake + 1) % 16))) {
|
||||
wake_on_can = ((msg->data[0] >> 5U) & 0x3U) != 0U;
|
||||
wake_on_can_cnt = 0U;
|
||||
}
|
||||
prev_counter_tesla_wake = counter;
|
||||
} else {
|
||||
prev_counter_tesla_wake = -1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Tesla Model S pre-AP exception
|
||||
if (msg_matches(msg, 0x101U, 0U, 3U)) {
|
||||
// Validate Tesla checksum/counter to avoid false positives on overlapping frames.
|
||||
int counter = msg->data[1] & 0xFU;
|
||||
int checksum = (((msg->addr & 0xFFU) + ((msg->addr >> 8U) & 0xFFU) + msg->data[0] + msg->data[1]) & 0xFFU);
|
||||
|
||||
static int prev_counter_tesla_preap = -1;
|
||||
if ((msg->data[2] == checksum) && (counter == ((prev_counter_tesla_preap + 1) % 16)) && (prev_counter_tesla_preap != -1)) {
|
||||
// GTW_epasPowerMode=1 is DRIVE_ON, which is the only ignition source on pre-AP cars.
|
||||
int power_mode = (msg->data[0] >> 3U) & 0xFU;
|
||||
ignition_can = power_mode == 0x1U;
|
||||
ignition_can_cnt = 0U;
|
||||
}
|
||||
prev_counter_tesla_preap = counter;
|
||||
}
|
||||
|
||||
// Mazda exception
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "opendbc/safety/declarations.h"
|
||||
#include "opendbc/safety/modes/ford_aol.h"
|
||||
#include "opendbc/bluepilot_lateral/safety/ford_bp.h"
|
||||
|
||||
// StarPilot's extended Ford curvature enforcement below is substantially adapted from
|
||||
// BluePilot bp-7.0 panda work, principally Alan Polk's 8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d
|
||||
@@ -115,7 +116,6 @@ static const CurvatureSteeringLimits FORD_LKA_STEERING_LIMITS = {
|
||||
.max_steer_power = 0,
|
||||
};
|
||||
|
||||
static bool ford_stock_switch = false;
|
||||
static bool ford_cancel_resume_button = false;
|
||||
|
||||
static bool ford_generic_canfd_extended = false;
|
||||
@@ -217,6 +217,7 @@ static void ford_rx_hook(const CANPacket_t *msg) {
|
||||
// Physical combined cancel/resume switch, not the outgoing resume signal.
|
||||
ford_cancel_resume_button = (msg->data[2] & 0x20U) != 0U;
|
||||
}
|
||||
ford_bp_rx(msg);
|
||||
}
|
||||
|
||||
static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
@@ -284,7 +285,7 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
// Safety check for Lane_Assist_Data1 action
|
||||
if (msg->addr == FORD_Lane_Assist_Data1) {
|
||||
if ((msg->addr == FORD_Lane_Assist_Data1) && !ford_bp_enabled) {
|
||||
if (ford_mach_e_extended || ford_explorer_extended) {
|
||||
tx &= (msg->data[4] & 0x1U) == 0U;
|
||||
}
|
||||
@@ -348,7 +349,7 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
// Safety check for LateralMotionControl action
|
||||
if (msg->addr == FORD_LateralMotionControl) {
|
||||
if ((msg->addr == FORD_LateralMotionControl) && !ford_bp_enabled) {
|
||||
// Signal: LatCtl_D_Rq
|
||||
bool steer_control_enabled = ((msg->data[4] >> 2) & 0x7U) != 0U;
|
||||
unsigned int raw_curvature = (msg->data[0] << 3) | (msg->data[1] >> 5);
|
||||
@@ -388,7 +389,7 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
// Safety check for LateralMotionControl2 action
|
||||
if (msg->addr == FORD_LateralMotionControl2) {
|
||||
if ((msg->addr == FORD_LateralMotionControl2) && !ford_bp_enabled) {
|
||||
// Signal: LatCtl_D2_Rq
|
||||
bool steer_control_enabled = ((msg->data[0] >> 4) & 0x7U) != 0U;
|
||||
unsigned int raw_curvature = (msg->data[2] << 3) | (msg->data[3] >> 5);
|
||||
@@ -472,6 +473,10 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
}
|
||||
|
||||
if (ford_bp_enabled) {
|
||||
if (msg->addr == FORD_Lane_Assist_Data1) { tx &= ford_bp_lka(msg); }
|
||||
if ((msg->addr == FORD_LateralMotionControl) || (msg->addr == FORD_LateralMotionControl2)) { tx &= ford_bp_lateral(msg); }
|
||||
}
|
||||
ford_aol_tx(msg, tx);
|
||||
return tx;
|
||||
}
|
||||
@@ -571,11 +576,13 @@ static safety_config ford_init(uint16_t param) {
|
||||
#ifdef ALLOW_DEBUG
|
||||
ford_mach_e_extended |= (param == 19U) && (((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param)));
|
||||
#endif
|
||||
ford_stock_switch = (((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param))) &&
|
||||
const bool bp_config_valid = ford_bp_configure(param);
|
||||
ford_stock_switch = ((((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param))) &&
|
||||
((param == 2U) || (param == 8U) || (param == 10U) || (param == 12U) ||
|
||||
((param == 18U) && ford_mach_e_extended) ||
|
||||
((param == 32U) && ford_explorer_extended) ||
|
||||
((param == 66U) && ford_generic_canfd_extended));
|
||||
((param == 66U) && ford_generic_canfd_extended))) ||
|
||||
(ford_bp_enabled && ((param & 1U) == 0U));
|
||||
ford_cancel_resume_button = false;
|
||||
ford_mach_e_path_angle_last = 0;
|
||||
ford_mach_e_announced = false;
|
||||
@@ -615,6 +622,7 @@ static safety_config ford_init(uint16_t param) {
|
||||
if (ford_explorer_extended && (param == 32U)) {
|
||||
SET_TX_MSGS(FORD_STOCK_TX_MSGS, ret);
|
||||
}
|
||||
if (ford_bp_enabled && (param == 128U)) { SET_TX_MSGS(FORD_STOCK_TX_MSGS, ret); }
|
||||
if (ford_stock_switch) {
|
||||
if (ford_lka_steering) {
|
||||
SET_RX_CHECKS(ford_stock_lka_rx_checks, ret);
|
||||
@@ -622,7 +630,7 @@ static safety_config ford_init(uint16_t param) {
|
||||
SET_RX_CHECKS(ford_stock_rx_checks, ret);
|
||||
}
|
||||
}
|
||||
if ((mach_e_namespace && !ford_mach_e_extended) ||
|
||||
if (!bp_config_valid || (mach_e_namespace && !ford_mach_e_extended) ||
|
||||
(explorer_namespace && !ford_explorer_extended) ||
|
||||
(generic_canfd_namespace && !ford_generic_canfd_extended)) {
|
||||
ret.tx_msgs = NULL;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
static bool honda_stock_aol_enabled = false;
|
||||
static bool honda_stock_aol_token = false;
|
||||
static bool honda_stock_aol_pending = false;
|
||||
static uint32_t honda_stock_aol_grant_ts = 0U;
|
||||
static bool honda_stock_aol_main_neutral = false;
|
||||
static bool honda_stock_aol_lkas_neutral = false;
|
||||
static bool honda_stock_aol_lkas_previous = false;
|
||||
@@ -18,10 +20,20 @@ static RxCheck honda_rx_workspace[7];
|
||||
|
||||
static void honda_stock_aol_clear(void) {
|
||||
honda_stock_aol_token = false;
|
||||
honda_stock_aol_pending = false;
|
||||
honda_stock_aol_main_neutral = false;
|
||||
honda_stock_aol_lkas_neutral = false;
|
||||
}
|
||||
|
||||
// A new physical grant may precede the host engagement heartbeat. It grants no output
|
||||
// until an engaged heartbeat and a fresh host request acknowledge this gesture.
|
||||
static void honda_stock_aol_grant(void) {
|
||||
honda_stock_aol_token = true;
|
||||
honda_stock_aol_pending = !heartbeat_engaged;
|
||||
honda_stock_aol_grant_ts = microsecond_timer_get();
|
||||
honda_stock_aol_session = heartbeat_engaged;
|
||||
}
|
||||
|
||||
static void honda_stock_aol_reset(void) {
|
||||
honda_stock_aol_clear();
|
||||
honda_stock_aol_enabled = false;
|
||||
@@ -55,14 +67,25 @@ static bool honda_stock_aol_sources_current(void) {
|
||||
|
||||
static uint8_t honda_stock_aol_request_mask(void) {
|
||||
uint8_t request = 0U;
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
const uint32_t grant_age = safety_get_ts_elapsed(now, honda_stock_aol_grant_ts);
|
||||
const bool pending_expired = honda_stock_aol_pending && (grant_age > AOL_HOST_REQUEST_TIMEOUT_US);
|
||||
const bool expired = honda_stock_aol_request_seen &&
|
||||
(safety_get_ts_elapsed(microsecond_timer_get(), aol_host_request_ts) > AOL_HOST_REQUEST_TIMEOUT_US);
|
||||
if (relay_malfunction || expired || safety_rx_checks_invalid ||
|
||||
(honda_stock_aol_session && (!heartbeat_engaged || !aol_rx_healthy() || !honda_stock_aol_sources_current()))) {
|
||||
(safety_get_ts_elapsed(now, aol_host_request_ts) > AOL_HOST_REQUEST_TIMEOUT_US);
|
||||
if (relay_malfunction || expired || pending_expired || safety_rx_checks_invalid ||
|
||||
(honda_stock_aol_session && !heartbeat_engaged) ||
|
||||
((honda_stock_aol_session || honda_stock_aol_pending) &&
|
||||
(!aol_rx_healthy() || !honda_stock_aol_sources_current()))) {
|
||||
honda_stock_aol_clear();
|
||||
honda_stock_aol_session = false;
|
||||
} else if (heartbeat_engaged && aol_rx_healthy() && honda_stock_aol_sources_current()) {
|
||||
honda_stock_aol_session = true;
|
||||
request = aol_host_axis_mask;
|
||||
const bool pending_ack = !honda_stock_aol_pending || ((aol_host_axis_mask != 0U) &&
|
||||
(safety_get_ts_elapsed(now, aol_host_request_ts) <= grant_age));
|
||||
if (pending_ack) {
|
||||
honda_stock_aol_pending = false;
|
||||
honda_stock_aol_session = true;
|
||||
request = aol_host_axis_mask;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
return request;
|
||||
@@ -94,6 +117,13 @@ static bool honda_stock_aol_selected(const CANPacket_t *msg, const RxCheck *chec
|
||||
|
||||
static void honda_stock_aol_rx(const CANPacket_t *msg, unsigned int pt_bus, bool normal_engagement) {
|
||||
if (honda_stock_aol_enabled) {
|
||||
// Retire the old session before interpreting a new validated physical edge.
|
||||
// The following gesture may create a bounded pending grant; a status read
|
||||
// must not mistake that new grant for the retired session.
|
||||
if (honda_stock_aol_session && !heartbeat_engaged) {
|
||||
honda_stock_aol_clear();
|
||||
honda_stock_aol_session = false;
|
||||
}
|
||||
const unsigned int main_addr = honda_stock_aol_alt_main ? 0x1A6U : 0x326U;
|
||||
const unsigned int buttons_addr = honda_stock_aol_alt_main ? 0x1A6U : 0x296U;
|
||||
const unsigned int buttons_len = honda_stock_aol_alt_main ? 8U : 4U;
|
||||
@@ -104,7 +134,7 @@ static void honda_stock_aol_rx(const CANPacket_t *msg, unsigned int pt_bus, bool
|
||||
honda_stock_aol_clear();
|
||||
honda_stock_aol_main_neutral = true;
|
||||
} else if (honda_stock_aol_main_neutral) {
|
||||
honda_stock_aol_token = true;
|
||||
honda_stock_aol_grant();
|
||||
honda_stock_aol_main_neutral = false;
|
||||
} else {
|
||||
}
|
||||
@@ -115,7 +145,7 @@ static void honda_stock_aol_rx(const CANPacket_t *msg, unsigned int pt_bus, bool
|
||||
if (!lkas) {
|
||||
honda_stock_aol_lkas_neutral = true;
|
||||
} else if (honda_stock_aol_lkas_neutral && !honda_stock_aol_lkas_previous && acc_main_on) {
|
||||
honda_stock_aol_token = true;
|
||||
honda_stock_aol_grant();
|
||||
honda_stock_aol_lkas_neutral = false;
|
||||
} else {
|
||||
}
|
||||
@@ -152,10 +182,9 @@ static void honda_stock_aol_rx(const CANPacket_t *msg, unsigned int pt_bus, bool
|
||||
(honda_stock_aol_eps == 3U) || (honda_stock_aol_eps == 4U) || (honda_stock_aol_eps == 6U);
|
||||
if (normal_engagement && engagement_source && controls_allowed && acc_main_on && honda_stock_aol_drive &&
|
||||
eps_known && !brake_pressed && !regen_braking && !steering_disengage &&
|
||||
heartbeat_engaged && !relay_malfunction && !safety_rx_checks_invalid && request_current &&
|
||||
!relay_malfunction && !safety_rx_checks_invalid && request_current &&
|
||||
aol_rx_healthy() && honda_stock_aol_sources_current()) {
|
||||
honda_stock_aol_token = true;
|
||||
honda_stock_aol_session = true;
|
||||
honda_stock_aol_grant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,9 +637,9 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
|
||||
static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
|
||||
static uint8_t hyundai_canfd_ioniq6_lfa_mirror[4] = {0};
|
||||
const TorqueSteeringLimits HYUNDAI_CANFD_STEERING_LIMITS = {
|
||||
.max_torque = 270,
|
||||
.max_rt_delta = 112,
|
||||
.max_rate_up = 2,
|
||||
.max_torque = 409,
|
||||
.max_rt_delta = 375,
|
||||
.max_rate_up = 10,
|
||||
.max_rate_down = 3,
|
||||
.driver_torque_allowance = 250,
|
||||
.driver_torque_multiplier = 2,
|
||||
@@ -677,9 +677,11 @@ static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
|
||||
bool tx = true;
|
||||
|
||||
// steering
|
||||
const bool torque_lka_long = hyundai_canfd_ioniq6_long || hyundai_canfd_torque_ev_long ||
|
||||
(hyundai_canfd_lka_steer_msg && hyundai_longitudinal && !hyundai_canfd_angle_steering);
|
||||
const unsigned int steer_addr = (hyundai_canfd_lka_steer_msg && !hyundai_longitudinal) ? hyundai_canfd_get_lka_addr() : 0x12aU;
|
||||
if (msg->addr == steer_addr) {
|
||||
if (hyundai_canfd_ioniq6_long || hyundai_canfd_torque_ev_long) {
|
||||
if (torque_lka_long) {
|
||||
// Even a rejected replacement invalidates an older unpaired command.
|
||||
hyundai_canfd_ioniq6_lfa_unpaired = false;
|
||||
}
|
||||
@@ -720,7 +722,7 @@ static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
|
||||
if (steer_torque_cmd_checks(desired_torque, steer_req, HYUNDAI_CANFD_STEERING_LIMITS)) {
|
||||
tx = false;
|
||||
}
|
||||
if (hyundai_canfd_ioniq6_long || hyundai_canfd_torque_ev_long) {
|
||||
if (torque_lka_long) {
|
||||
// This E-CAN command is the sole limited steering authority. Other
|
||||
// angle/enable fields in the same payload remain the host's fixed
|
||||
// neutral status, and CRC must cover the exact wire bytes.
|
||||
@@ -729,7 +731,7 @@ static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
|
||||
}
|
||||
tx &= hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU);
|
||||
}
|
||||
if ((hyundai_canfd_ioniq6_long || hyundai_canfd_torque_ev_long) && tx) {
|
||||
if ((torque_lka_long) && tx) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
hyundai_canfd_ioniq6_lfa_mirror[i] = msg->data[i + 3];
|
||||
}
|
||||
@@ -743,7 +745,7 @@ static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
|
||||
// by the identical steering/status request on A-CAN. The second channel may
|
||||
// mirror only that single fresh, accepted command; it must not run the global
|
||||
// torque limiter a second time or create independent steering authority.
|
||||
if ((hyundai_canfd_ioniq6_long || hyundai_canfd_torque_ev_long) && (msg->addr == hyundai_canfd_get_lka_addr())) {
|
||||
if ((torque_lka_long) && (msg->addr == hyundai_canfd_get_lka_addr())) {
|
||||
bool mirror = hyundai_canfd_ioniq6_lfa_unpaired &&
|
||||
safety_get_ts_elapsed(microsecond_timer_get(), hyundai_canfd_ioniq6_lfa_ts) <= 10000U;
|
||||
const int mirror_torque = (((msg->data[6] & 0xFU) << 7U) | (msg->data[5] >> 1U)) - 1024U;
|
||||
@@ -1539,7 +1541,7 @@ static safety_config hyundai_canfd_init_validated(uint16_t param) {
|
||||
};
|
||||
|
||||
ret = BUILD_SAFETY_CFG(hyundai_canfd_lka_steer_msg_long_rx_checks, HYUNDAI_CANFD_LKA_STEER_MSG_LONG_TX_MSGS);
|
||||
if ((param == 0x0095U) || (param == 0x0895U)) {
|
||||
if (hyundai_canfd_lka_steer_msg_alt) {
|
||||
SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_LONG_TX_MSGS, ret);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -92,10 +92,12 @@ ffi.cdef("void mutation_set_active_mutant(int id); int mutation_get_active_mutan
|
||||
class LibSafety:
|
||||
pass
|
||||
libsafety: LibSafety
|
||||
libpath: str
|
||||
|
||||
def load(path):
|
||||
global libsafety
|
||||
global libsafety, libpath
|
||||
libsafety = ffi.dlopen(str(path))
|
||||
libpath = str(Path(path).resolve())
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "libsafety":
|
||||
|
||||
@@ -24,6 +24,8 @@ import tree_sitter as ts
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SAFETY_DIR = ROOT / "opendbc" / "safety"
|
||||
BLUEPILOT_SAFETY_DIR = ROOT / "opendbc" / "bluepilot_lateral" / "safety"
|
||||
SAFETY_SOURCE_DIRS = (SAFETY_DIR, BLUEPILOT_SAFETY_DIR)
|
||||
SAFETY_TESTS_DIR = ROOT / "opendbc" / "safety" / "tests"
|
||||
SAFETY_C_REL = Path("opendbc/safety/tests/libsafety/safety.c")
|
||||
|
||||
@@ -274,7 +276,7 @@ def enumerate_sites(input_source, preprocessed_file):
|
||||
if mapped is None:
|
||||
continue
|
||||
origin_file, origin_line = mapped
|
||||
if SAFETY_DIR not in origin_file.parents and origin_file != SAFETY_DIR:
|
||||
if not any(origin_file.is_relative_to(source_dir) for source_dir in SAFETY_SOURCE_DIRS):
|
||||
continue
|
||||
site_id = len(out)
|
||||
site = MutationSite(
|
||||
@@ -460,11 +462,12 @@ def build_priority_tests(site, catalog, core_tests):
|
||||
"""
|
||||
src = site.origin_file
|
||||
rel_parts = src.relative_to(ROOT).parts
|
||||
is_mode = len(rel_parts) >= 4 and rel_parts[:3] == ("opendbc", "safety", "modes")
|
||||
is_mode = (len(rel_parts) >= 4 and rel_parts[:3] == ("opendbc", "safety", "modes")) or src.parent == BLUEPILOT_SAFETY_DIR
|
||||
|
||||
if is_mode:
|
||||
family = src.stem.split("_", 1)[0]
|
||||
family_modules = [name for name in sorted(catalog) if name == f"test_{family}.py" or name.startswith(f"test_{family}_")]
|
||||
family_modules = [name for name in sorted(catalog) if name == f"test_{family}.py" or name.startswith(f"test_{family}_") or
|
||||
(family == "ford" and name == "test_angle_native.py")]
|
||||
if family_modules:
|
||||
return [test_id for name in family_modules for test_id in catalog[name]]
|
||||
# Unmapped mode files still exercise the full safety suite rather than receive zero targets.
|
||||
@@ -526,7 +529,9 @@ def print_live_status(text, *, final=False):
|
||||
def _discover_test_catalog():
|
||||
loader = unittest.TestLoader()
|
||||
catalog = {}
|
||||
for test_file in sorted(SAFETY_TESTS_DIR.glob("test_*.py")):
|
||||
test_files = [*sorted(SAFETY_TESTS_DIR.glob("test_*.py")),
|
||||
ROOT / "opendbc" / "bluepilot_lateral" / "tests" / "test_angle_native.py"]
|
||||
for test_file in test_files:
|
||||
module_name = ".".join(test_file.relative_to(ROOT).with_suffix("").parts)
|
||||
suite = loader.loadTestsFromName(module_name)
|
||||
catalog[test_file.name] = [t.id() for group in suite for t in group]
|
||||
|
||||
@@ -574,7 +574,7 @@ class TestGmBoltPedalSafety(unittest.TestCase):
|
||||
bolt_pedal_main_ts_nanos=car_state.bolt_pedal_main_ts_nanos,
|
||||
cam_lka_steering_cmd_counter=0, loopback_lka_steering_cmd_updated=False,
|
||||
loopback_lka_steering_cmd_ts_nanos=1_000_000_000, pt_lka_steering_cmd_counter=0,
|
||||
buttons_counter=0)
|
||||
buttons_counter=0, steering_authority=car_state.steering_authority)
|
||||
_, commands = controller.update(control.as_reader(), cs, 1_000_000_000)
|
||||
acc_pedal = candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL
|
||||
self.assertEqual([msg[0] for msg in commands], ([0x3D1] if not acc_pedal else []) +
|
||||
|
||||
@@ -110,15 +110,13 @@ class TestHondaStockAol(unittest.TestCase):
|
||||
|
||||
def test_blocked_normal_edge_is_not_replayed_after_sources_recover(self):
|
||||
for case in CASES:
|
||||
for blocked in ("eps", "reverse", "heartbeat", "brake", "missing", "expired", "request_expired"):
|
||||
for blocked in ("eps", "reverse", "brake", "missing", "expired", "request_expired"):
|
||||
with self.subTest(case=case[0], blocked=blocked):
|
||||
self.configure(case)
|
||||
self.stream(eps=7 if blocked == "eps" else 0, gear=2 if blocked == "reverse" else None,
|
||||
brake=int(blocked == "brake"), omit="STEER_STATUS" if blocked == "missing" else None)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
if blocked == "heartbeat":
|
||||
self.safety.set_aol_test_heartbeat(False)
|
||||
elif blocked in ("expired", "request_expired"):
|
||||
if blocked in ("expired", "request_expired"):
|
||||
self.safety.set_timer(1_300_001)
|
||||
if blocked == "expired":
|
||||
self.safety.aol_set_host_request(1)
|
||||
@@ -235,3 +233,120 @@ class TestHondaStockAol(unittest.TestCase):
|
||||
self.stream(main=False)
|
||||
self.stream(main=True, lkas=1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
|
||||
def test_cold_valid_normal_grant_precedes_engaged_heartbeat(self):
|
||||
for case in CASES:
|
||||
for button in ((3, 4) if case[2] in (10, 11) else (3,)):
|
||||
with self.subTest(case=case[0], button=button):
|
||||
self.configure(case)
|
||||
self.safety.set_aol_test_heartbeat(False)
|
||||
self.stream(request=False)
|
||||
self.normal_engage(button=button)
|
||||
self.assertEqual(self.safety.aol_get_request_mask(), 0)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
self.safety.set_timer(1_010_000)
|
||||
self.safety.aol_set_host_request(3)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 3 if self.scoped else 0)
|
||||
self.press_brake()
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 1 if self.scoped else 0)
|
||||
|
||||
def test_prior_session_new_lkas_survives_status_before_request_and_heartbeat(self):
|
||||
for case in CASES:
|
||||
with self.subTest(case=case[0]):
|
||||
self.configure(case)
|
||||
self.stream()
|
||||
self.normal_engage()
|
||||
self.safety.aol_set_host_request(3)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 3 if self.scoped else 0)
|
||||
self.press_brake()
|
||||
self.safety.set_aol_test_heartbeat(False)
|
||||
self.safety.aol_set_host_request(0)
|
||||
self.safety.set_timer(1_010_000)
|
||||
self.stream(cruise=1, request=False) # New neutral after old session loss.
|
||||
self.stream(cruise=1, lkas=1, request=False)
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.assertEqual(self.safety.aol_get_request_mask(), 0) # Actual first status order.
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
self.safety.set_timer(1_020_000)
|
||||
self.safety.aol_set_host_request(1)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.assertEqual(self.safety.aol_get_request_mask(), 1 if self.scoped else 0)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 1 if self.scoped else 0)
|
||||
self.safety.set_aol_test_heartbeat(False)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0) # Active grant still revoked.
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.stream(cruise=1, lkas=1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0) # Held button cannot replay.
|
||||
|
||||
def test_pending_physical_grant_requires_post_gesture_request(self):
|
||||
for case in CASES:
|
||||
with self.subTest(case=case[0]):
|
||||
self.configure(case)
|
||||
self.safety.set_aol_test_heartbeat(False)
|
||||
self.stream()
|
||||
self.safety.set_timer(1_010_000)
|
||||
self.stream(lkas=1, request=False)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0) # Previous request is insufficient.
|
||||
self.safety.set_timer(1_020_000)
|
||||
self.safety.aol_set_host_request(1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 1 if self.scoped else 0)
|
||||
|
||||
def test_expired_pending_heartbeat_and_sources_require_new_gesture(self):
|
||||
for case in CASES:
|
||||
for loss in ("heartbeat_expired", "eps_source", "gear_source", "main_source", "host_expired", "relay", "eps_invalid", "rx_checksum"):
|
||||
with self.subTest(case=case[0], loss=loss):
|
||||
self.configure(case)
|
||||
self.safety.set_aol_test_heartbeat(False)
|
||||
self.stream(request=False)
|
||||
self.normal_engage()
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
self.safety.aol_set_host_request(0)
|
||||
if loss in ("heartbeat_expired", "eps_source", "gear_source", "main_source", "host_expired"):
|
||||
self.safety.set_timer(1_300_001)
|
||||
source = {"eps_source": "STEER_STATUS", "gear_source": case[6],
|
||||
"main_source": "SCM_BUTTONS" if self.alt else "SCM_FEEDBACK"}.get(loss)
|
||||
self.stream(cruise=1, omit=source, request=False)
|
||||
if loss != "host_expired":
|
||||
self.safety.aol_set_host_request(0)
|
||||
elif loss == "relay":
|
||||
self.safety.set_relay_malfunction(True)
|
||||
elif loss == "rx_checksum":
|
||||
invalid = self.packet("POWERTRAIN_DATA", {"ACC_STATUS": 1})
|
||||
invalid.data[7] ^= 1
|
||||
self.assertFalse(self.safety.safety_rx_hook(invalid))
|
||||
else:
|
||||
self.stream(cruise=1, eps=7, request=False)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
self.safety.set_relay_malfunction(False)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.stream(cruise=1)
|
||||
self.press_brake()
|
||||
self.safety.aol_set_host_request(3)
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
self.stream(cruise=0)
|
||||
self.normal_engage()
|
||||
self.press_brake()
|
||||
self.safety.aol_set_host_request(1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 1 if self.scoped else 0)
|
||||
|
||||
def test_pending_grant_clock_wrap_and_deadline(self):
|
||||
for case in CASES:
|
||||
for delay in (100_000, 300_000, 300_001):
|
||||
with self.subTest(case=case[0], delay=delay):
|
||||
self.configure(case)
|
||||
self.safety.set_timer(0xffff_0000)
|
||||
self.safety.set_aol_test_heartbeat(False)
|
||||
self.stream(request=False)
|
||||
self.stream(lkas=1, request=False)
|
||||
self.safety.set_timer((0xffff_0000 + delay) & 0xffff_ffff)
|
||||
self.stream(lkas=1, request=False) # Actual fresh CAN, held level, no new edge.
|
||||
self.safety.aol_set_host_request(0)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(), 0)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.safety.aol_set_host_request(1)
|
||||
self.assertEqual(self.safety.aol_get_permission_mask(),
|
||||
int(self.scoped and delay <= 300_000))
|
||||
|
||||
@@ -31,11 +31,11 @@ class TestHyundaiCanfdBase(HyundaiButtonBase, common.CarSafetyTest, common.Drive
|
||||
STANDSTILL_THRESHOLD = 12 # 0.375 kph
|
||||
FWD_BLACKLISTED_ADDRS = {2: [0x50, 0x2a4]}
|
||||
|
||||
MAX_RATE_UP = 2
|
||||
MAX_RATE_UP = 10
|
||||
MAX_RATE_DOWN = 3
|
||||
MAX_TORQUE_LOOKUP = [0], [270]
|
||||
MAX_TORQUE_LOOKUP = [0], [409]
|
||||
|
||||
MAX_RT_DELTA = 112
|
||||
MAX_RT_DELTA = 375
|
||||
|
||||
DRIVER_TORQUE_ALLOWANCE = 250
|
||||
DRIVER_TORQUE_FACTOR = 2
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.hyundai.hyundaicanfd import create_steering_messages
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags, CarControllerParams
|
||||
from opendbc.car.lateral import apply_driver_steer_torque_limits
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
class TestHyundaiCanfdTorqueEnvelope(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.release = self.safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
|
||||
self.packer = CANPacker('hyundai_canfd_generated')
|
||||
|
||||
def tearDown(self):
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(0)
|
||||
self.safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
|
||||
|
||||
def mode(self, word):
|
||||
self.safety.set_alternative_experience(0)
|
||||
self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, word), 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_timer(0)
|
||||
self.word = word
|
||||
|
||||
def tx(self, frame):
|
||||
return bool(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1])))
|
||||
|
||||
def frames(self, torque):
|
||||
flags = HyundaiFlags.CANFD | (HyundaiFlags.CANFD_LKA_STEER_MSG if self.word & 16 else 0)
|
||||
if self.word & 128:
|
||||
flags |= HyundaiFlags.CANFD_LKA_STEER_MSG_ALT
|
||||
cp = SimpleNamespace(carFingerprint=CAR.HYUNDAI_TUCSON_4TH_GEN, flags=int(flags),
|
||||
openpilotLongitudinalControl=bool(self.word & 4) and not self.release)
|
||||
return create_steering_messages(self.packer, cp, SimpleNamespace(ECAN=1 if self.word & 16 else 0, ACAN=0), True, True, torque)
|
||||
|
||||
def previous(self, torque, driver=0):
|
||||
self.safety.set_desired_torque_last(torque)
|
||||
self.safety.set_rt_torque_last(torque)
|
||||
for counter in range(6):
|
||||
frame = self.packer.make_can_msg('MDPS', 1 if self.word & 16 else 0,
|
||||
{'COUNTER': counter, 'MDPS_StrTqSnsrVal': driver, 'MDPS_OutTqVal': 0})
|
||||
self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1])))
|
||||
self.safety.set_controls_allowed(True)
|
||||
|
||||
def test_host_unwind_speed_transition_and_driver_override(self):
|
||||
for word in (0x11, 0x91, 9):
|
||||
for vehicle in (CAR.KIA_EV6, CAR.KIA_CARNIVAL_HEV_4TH_GEN):
|
||||
for sign in (-1, 1):
|
||||
for speed, expected in ((14.999, 401 if vehicle == CAR.KIA_EV6 else 406), (15., 406)):
|
||||
with self.subTest(word=hex(word), vehicle=vehicle, sign=sign, speed=speed):
|
||||
self.mode(word)
|
||||
self.previous(sign * 409, -sign * 500)
|
||||
cp = SimpleNamespace(flags=int(HyundaiFlags.CANFD), carFingerprint=vehicle)
|
||||
output = apply_driver_steer_torque_limits(0, sign * 409, -sign * 500, CarControllerParams(cp, speed))
|
||||
self.assertEqual(output, sign * expected)
|
||||
self.assertTrue(self.tx(self.frames(output)[0]))
|
||||
if speed < 15 and vehicle == CAR.KIA_EV6:
|
||||
self.safety.set_timer(10_000)
|
||||
next_output = apply_driver_steer_torque_limits(0, output, -sign * 500, CarControllerParams(cp, 15.))
|
||||
self.assertEqual(next_output, sign * 398)
|
||||
self.assertTrue(self.tx(self.frames(next_output)[0]))
|
||||
self.previous(sign * 409, -sign * 500)
|
||||
self.assertFalse(self.tx(self.frames(sign * 407)[0]))
|
||||
self.previous(sign)
|
||||
crossing = apply_driver_steer_torque_limits(-sign * 409, sign, 0, CarControllerParams(cp, speed))
|
||||
self.assertEqual(crossing, sign * (1 - (8 if speed < 15 and vehicle == CAR.KIA_EV6 else 3)))
|
||||
self.assertTrue(self.tx(self.frames(crossing)[0]))
|
||||
|
||||
def test_real_clock_ramp_and_absolute_cap(self):
|
||||
for word in (0x11, 0x91, 9):
|
||||
for sign in (-1, 1):
|
||||
with self.subTest(word=hex(word), sign=sign):
|
||||
self.mode(word)
|
||||
self.previous(0)
|
||||
self.assertFalse(self.tx(self.frames(sign * 409)[0]))
|
||||
self.previous(0)
|
||||
for tick in range(42):
|
||||
self.safety.set_timer(tick * 10_000)
|
||||
self.assertTrue(self.tx(self.frames(sign * min(tick * 10, 409))[0]))
|
||||
self.previous(sign * 409)
|
||||
self.assertFalse(self.tx(self.frames(sign * 410)[0]))
|
||||
self.previous(0)
|
||||
self.assertFalse(self.tx(self.frames(sign * 11)[0]))
|
||||
for clock in (250_000, 250_001):
|
||||
self.mode(word)
|
||||
self.previous(sign * 375)
|
||||
self.safety.set_rt_torque_last(0)
|
||||
self.safety.set_timer(clock)
|
||||
self.assertTrue(self.tx(self.frames(sign * 375)[0]))
|
||||
self.assertEqual(self.tx(self.frames(sign * 376)[0]), clock > 250_000)
|
||||
|
||||
def test_all_hda2_long_secondary_frames_require_single_limited_primary(self):
|
||||
for word in (0x14, 0x15, 0x16, 0x34, 0x35, 0x36, 0x94, 0x95, 0x96, 0xB4, 0xB5, 0xB6):
|
||||
for sign in (-1, 1):
|
||||
for fault in ('unpaired', 'different', 'expired', 'replacement', 'checksum', 'body'):
|
||||
with self.subTest(word=hex(word), sign=sign, fault=fault):
|
||||
self.mode(word)
|
||||
self.previous(sign * 409)
|
||||
frames = self.frames(sign * 409)
|
||||
if self.release:
|
||||
self.assertEqual(len(frames), 1)
|
||||
self.assertTrue(self.tx(frames[0]))
|
||||
lfa = self.packer.make_can_msg('LFA', 1, {'StrTqReqVal': sign * 409, 'ActToiSta': 1})
|
||||
self.assertFalse(self.tx(lfa))
|
||||
continue
|
||||
source, mirror = frames
|
||||
self.assertFalse(self.tx(mirror))
|
||||
self.previous(sign * 409)
|
||||
self.assertTrue(self.tx(source))
|
||||
if fault == 'unpaired':
|
||||
self.assertTrue(self.tx(mirror))
|
||||
elif fault == 'different':
|
||||
self.assertFalse(self.tx(self.frames(sign * 384)[1]))
|
||||
elif fault == 'expired':
|
||||
self.safety.set_timer(10_001)
|
||||
self.assertFalse(self.tx(mirror))
|
||||
elif fault == 'replacement':
|
||||
self.assertFalse(self.tx(self.frames(sign * 410)[0]))
|
||||
elif fault == 'body':
|
||||
name = 'LKAS_ALT' if word & 128 else 'LKAS'
|
||||
forged = self.packer.make_can_msg(name, 0, {'LKA_OptUsmSta': 2, 'LKA_SysIndReq': 2,
|
||||
'StrTqReqVal': sign * 409, 'ActToiSta': 1, 'Damping_Gain': 99})
|
||||
self.assertFalse(self.tx(forged))
|
||||
else:
|
||||
data = bytearray(mirror[1])
|
||||
data[0] ^= 1
|
||||
self.assertFalse(self.tx((mirror[0], bytes(data), mirror[2])))
|
||||
self.assertFalse(self.tx(mirror))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user