Define host-to-Panda AOL intent, policy and wire contracts with neutral defaults before vehicle adapters register capabilities.

Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
This commit is contained in:
firestar5683
2026-09-25 14:53:19 -05:00
parent ce6c209d38
commit f45cc97134
17 changed files with 1299 additions and 3 deletions
+173
View File
@@ -0,0 +1,173 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <ctime>
#include <limits>
#include <optional>
#include <string>
#ifdef __APPLE__
#include <mach/mach_time.h>
#endif
#include "panda/board/health.h"
constexpr uint64_t AOL_AXIS_MESSAGE_MAX_AGE_NS = 200000000ULL;
struct AolSafetyProfile {
uint8_t mode;
bool (*accepts_param)(uint16_t);
bool normal_runtime;
};
struct AolProfileRegistry {
const AolSafetyProfile *profiles;
size_t count;
const AolSafetyProfile *find(uint8_t mode) const {
if (profiles == nullptr) return nullptr;
for (size_t i = 0; i < count; ++i) {
if (profiles[i].mode == mode) return &profiles[i];
}
return nullptr;
}
const AolSafetyProfile *find(uint8_t mode, uint16_t param) const {
if (profiles == nullptr) return nullptr;
for (size_t i = 0; i < count; ++i) {
if (profiles[i].mode == mode && profiles[i].accepts_param != nullptr && profiles[i].accepts_param(param)) return &profiles[i];
}
return nullptr;
}
};
// Python messaging.new_message and the AOL host use time.monotonic_ns().
// Linux CLOCK_BOOTTIME includes suspend time; the regular pandad clocks stay
// unchanged, while this protocol uses the matching monotonic epoch only.
inline uint64_t aol_monotonic_ns() {
#ifdef __APPLE__
mach_timebase_info_data_t scale{};
if (mach_timebase_info(&scale) != KERN_SUCCESS || scale.denom == 0U) return 0U;
const __uint128_t ns = (static_cast<__uint128_t>(mach_absolute_time()) * scale.numer) / scale.denom;
return ns <= std::numeric_limits<uint64_t>::max() ? static_cast<uint64_t>(ns) : 0U;
#else
timespec ts{};
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0 || ts.tv_sec < 0 || ts.tv_nsec < 0 || ts.tv_nsec >= 1000000000L) return 0U;
const __uint128_t ns = static_cast<__uint128_t>(ts.tv_sec) * 1000000000U + ts.tv_nsec;
return ns <= std::numeric_limits<uint64_t>::max() ? static_cast<uint64_t>(ns) : 0U;
#endif
}
inline std::optional<aol_safety_health_t> parse_aol_status(const unsigned char *data, int count) {
if (data == nullptr || count != static_cast<int>(sizeof(aol_safety_health_t))) return std::nullopt;
aol_safety_health_t status{};
std::memcpy(&status, data, sizeof(status));
if (status.magic != AOL_SAFETY_PROTOCOL_MAGIC || status.version != AOL_SAFETY_PROTOCOL_VERSION) return std::nullopt;
return status;
}
inline bool aol_capable(const std::optional<aol_safety_health_t> &status, uint8_t expected_mode,
const AolProfileRegistry &registry) {
const AolSafetyProfile *profile = registry.find(expected_mode, status ? status->safety_param : 0U);
return status && profile && profile->accepts_param && (status->capability_flags & 0x1U) != 0U &&
status->safety_mode == expected_mode && profile->accepts_param(status->safety_param);
}
inline bool aol_runtime_enabled(bool replay, const std::optional<aol_safety_health_t> &status,
const AolProfileRegistry &registry) {
if (replay) return true;
if (!status) return false;
const AolSafetyProfile *profile = registry.find(status->safety_mode, status->safety_param);
return profile && profile->normal_runtime && aol_capable(status, profile->mode, registry);
}
struct AolAxisInput {
bool alive_valid = false;
bool qualified = false;
std::string session;
uint64_t observed_ns = 0;
uint64_t valid_until_ns = 0;
uint64_t message_ns = 0;
bool desired_lateral = false;
bool desired_longitudinal = false;
bool retain_lateral_arm = false;
bool optional_set_release = false;
};
struct AolWritePlan {
bool capable = false;
uint8_t request_mask = 0;
bool retain_lateral_arm = false;
};
struct AolOutcome {
bool compatible = false;
std::optional<aol_safety_health_t> status;
std::string session;
};
// One pandad owner negotiates a new host session with a native zero-request
// acknowledgement before allowing either independent axis to be requested.
class AolAxisNegotiator {
public:
explicit AolAxisNegotiator(const AolProfileRegistry &registry) : registry_(registry) {}
AolWritePlan prepare(const std::optional<aol_safety_health_t> &before, const AolAxisInput &axis,
uint64_t now_ns, uint8_t expected_mode) {
const uint16_t param = before ? before->safety_param : 0U;
const bool optional_capable = before && ((before->capability_flags & 0x2U) != 0U);
if (expected_mode != expected_mode_ || param != expected_param_ || optional_capable != optional_capable_) {
reset_pending_ = true;
}
expected_mode_ = expected_mode;
expected_param_ = param;
optional_capable_ = optional_capable;
if (!aol_capable(before, expected_mode, registry_)) {
reset_pending_ = true;
return {};
}
if (axis.alive_valid && axis.qualified && !axis.session.empty() &&
axis.observed_ns <= now_ns && now_ns <= axis.valid_until_ns &&
axis.message_ns <= now_ns && now_ns - axis.message_ns <= AOL_AXIS_MESSAGE_MAX_AGE_NS) {
if (axis.session != session_) {
session_ = axis.session;
reset_pending_ = true;
}
if (!reset_pending_) {
return {true, static_cast<uint8_t>((axis.desired_lateral ? 0x1U : 0U) |
(axis.desired_longitudinal ? 0x2U : 0U) |
((axis.optional_set_release && (before->capability_flags & 0x2U) != 0U &&
(before->safety_param == 0x815U || before->safety_param == 0x895U)) ? 0x4U : 0U)),
axis.retain_lateral_arm};
}
}
return {true, 0U};
}
AolOutcome complete(const AolWritePlan &plan, bool write_ok,
const std::optional<aol_safety_health_t> &after) {
if (!plan.capable || !write_ok || !aol_capable(after, expected_mode_, registry_)) {
reset_pending_ = true;
return {};
}
if (after->safety_param != expected_param_ ||
(((plan.request_mask & 0x4U) != 0U) && (after->capability_flags & 0x2U) == 0U)) {
reset_pending_ = true;
return {};
}
if (reset_pending_ && plan.request_mask == 0U && after->request_mask == 0U) {
reset_pending_ = false;
}
return {true, after, reset_pending_ ? "" : session_};
}
private:
AolProfileRegistry registry_;
std::string session_;
bool reset_pending_ = true;
uint8_t expected_mode_ = 0;
uint16_t expected_param_ = 0;
bool optional_capable_ = false;
};
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <capnp/message.h>
#include <capnp/serialize.h>
#include "openpilot/cereal/gen/cpp/custom.capnp.h"
// Inner bounded, flat Cap'n Proto payload for Event.aolSafetyWire @124 :Data.
// The outer Event validity remains a separate native status check.
struct AolPandaSlot {
uint16_t slot_index = 0;
std::string hardware_serial;
uint16_t safety_model = 0;
uint16_t safety_param = 0;
uint16_t alternative_experience = 0;
bool controls_allowed = false;
bool safety_rx_checks_invalid = false;
bool heartbeat_lost = false;
uint32_t faults = 0;
};
struct AolPandaStateSnapshot {
// Source Event BOOTTIME stamp: provenance only, not the wire's lease clock.
uint64_t source_mono_time = 0;
std::vector<AolPandaSlot> slots;
};
struct AolSafetyWireFields {
uint16_t protocol_version = 0;
bool compatible = false;
uint64_t observed_mono_time = 0;
uint64_t valid_until_mono_time = 0;
uint16_t safety_model = 0;
uint16_t safety_param = 0;
bool lateral_allowed = false;
bool longitudinal_allowed = false;
bool requested_lateral = false;
bool requested_longitudinal = false;
std::string panda_serial;
std::string axis_session_id;
AolPandaStateSnapshot inventory;
};
inline std::vector<uint8_t> encode_aol_safety_wire(const AolSafetyWireFields &fields) {
if (fields.panda_serial.size() > 96 || fields.axis_session_id.size() > 96 ||
fields.valid_until_mono_time < fields.observed_mono_time) return {};
if (fields.inventory.source_mono_time == 0U || fields.inventory.slots.empty() || fields.inventory.slots.size() > 2U) return {};
for (const auto &slot : fields.inventory.slots) {
if (slot.hardware_serial.empty() || slot.hardware_serial.size() > 96U) return {};
}
capnp::MallocMessageBuilder builder;
auto out = builder.initRoot<cereal::AolAxisState::SafetyWire>();
out.setKind(1U);
out.setVersion(2U);
out.setProtocolVersion(fields.protocol_version);
out.setCompatible(fields.compatible);
out.setObservedMonoTime(fields.observed_mono_time);
out.setValidUntilMonoTime(fields.valid_until_mono_time);
out.setSafetyModel(fields.safety_model);
out.setSafetyParam(fields.safety_param);
out.setLateralAllowed(fields.lateral_allowed);
out.setLongitudinalAllowed(fields.longitudinal_allowed);
out.setRequestedLateral(fields.requested_lateral);
out.setRequestedLongitudinal(fields.requested_longitudinal);
out.setPandaSerial(fields.panda_serial);
out.setAxisSessionId(fields.axis_session_id);
out.setSourcePandaStatesMonoTime(fields.inventory.source_mono_time);
auto inventory = out.initPandaInventory(fields.inventory.slots.size());
for (size_t i = 0; i < fields.inventory.slots.size(); ++i) {
const auto &slot = fields.inventory.slots[i];
auto entry = inventory[i];
entry.setSlotIndex(slot.slot_index);
entry.setHardwareSerial(slot.hardware_serial);
entry.setSafetyModel(slot.safety_model);
entry.setSafetyParam(slot.safety_param);
entry.setAlternativeExperience(slot.alternative_experience);
entry.setControlsAllowed(slot.controls_allowed);
entry.setSafetyRxChecksInvalid(slot.safety_rx_checks_invalid);
entry.setHeartbeatLost(slot.heartbeat_lost);
entry.setFaults(slot.faults);
}
auto flat = capnp::messageToFlatArray(builder);
auto bytes = flat.asBytes();
if (bytes.size() > 512) return {};
return {bytes.begin(), bytes.end()};
}
// Card's armed bit keeps a previously authorized session alive while its axis
// request is zero. It never grants an actuator request or native permission.
inline bool aol_armed_intent(kj::ArrayPtr<const capnp::byte> bytes, uint64_t message_ns,
uint64_t car_state_ns, uint64_t now_ns, bool optional_release = false) {
constexpr uint64_t MAX_AGE_NS = 30000000ULL;
if (bytes.size() < 16U || bytes.size() > 512U || bytes.size() % 8U != 0U ||
message_ns == 0U || car_state_ns == 0U || message_ns > now_ns ||
car_state_ns > now_ns || now_ns - message_ns > MAX_AGE_NS ||
(message_ns > car_state_ns ? message_ns - car_state_ns : car_state_ns - message_ns) > MAX_AGE_NS) return false;
uint32_t segments = 0U, words = 0U;
std::memcpy(&segments, bytes.begin(), sizeof(segments));
std::memcpy(&words, bytes.begin() + 4U, sizeof(words));
if (segments != 0U || words > 63U || bytes.size() != 8U + 8U * words) return false;
try {
auto aligned = kj::heapArray<capnp::word>(bytes.size() / sizeof(capnp::word));
std::memcpy(aligned.begin(), bytes.begin(), bytes.size());
capnp::ReaderOptions options;
options.traversalLimitInWords = 63U;
options.nestingLimit = 4;
capnp::FlatArrayMessageReader reader(aligned.asPtr(), options);
auto intent = reader.getRoot<cereal::AolAxisState::IntentWire>();
auto producer = intent.getProducerSessionId();
return intent.getKind() == 2U && intent.getVersion() == 1U &&
producer.size() > 0U && producer.size() <= 96U &&
intent.getSettingsQualified() && (optional_release ? intent.getOptionalSetRelease() : intent.getLateralArmed()) &&
intent.getCarStateLogMonoTime() == message_ns &&
intent.getObservedMonoTime() <= now_ns &&
now_ns - intent.getObservedMonoTime() <= MAX_AGE_NS &&
now_ns <= intent.getValidUntilMonoTime();
} catch (const kj::Exception &) {
return false;
}
}
+11
View File
@@ -1,4 +1,5 @@
#include "selfdrive/pandad/panda.h"
#include "selfdrive/pandad/aol_protocol.h"
#include <unistd.h>
@@ -90,6 +91,12 @@ std::optional<health_t> Panda::get_state() {
return err >= 0 ? std::make_optional(health) : std::nullopt;
}
std::optional<aol_safety_health_t> Panda::get_aol_safety_state() {
aol_safety_health_t status {0};
int count = handle->control_read(0xd5, 0, 0, (unsigned char*)&status, sizeof(status));
return parse_aol_status(reinterpret_cast<const unsigned char *>(&status), count);
}
std::optional<can_health_t> Panda::get_can_state(uint16_t can_number) {
can_health_t can_health {0};
int err = handle->control_read(0xc2, can_number, 0, (unsigned char*)&can_health, sizeof(can_health));
@@ -138,6 +145,10 @@ void Panda::send_heartbeat(bool engaged) {
handle->control_write(0xf3, engaged, 0);
}
bool Panda::set_aol_axis_request(uint8_t axis_mask) {
return handle->control_write(0xf5, axis_mask & 0x3U, 0) >= 0;
}
void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) {
handle->control_write(0xde, bus, (speed * 10));
}
+9 -3
View File
@@ -8,6 +8,7 @@
#include <optional>
#include <string>
#include <vector>
#include <utility>
#include "openpilot/cereal/gen/cpp/car.capnp.h"
#include "openpilot/cereal/gen/cpp/log.capnp.h"
@@ -45,10 +46,11 @@ struct can_frame {
class Panda {
private:
std::unique_ptr<PandaSpiHandle> handle;
std::unique_ptr<PandaCommsHandle> handle;
public:
Panda(std::string serial);
Panda(std::string serial, uint32_t bus_offset=0);
const uint32_t bus_offset;
cereal::PandaState::PandaType hw_type = cereal::PandaState::PandaType::UNKNOWN;
@@ -69,6 +71,7 @@ public:
uint16_t get_fan_speed();
void set_ir_pwr(uint16_t ir_pwr);
std::optional<health_t> get_state();
std::optional<aol_safety_health_t> get_aol_safety_state();
std::optional<can_health_t> get_can_state(uint16_t can_number);
void set_loopback(bool loopback);
std::optional<std::vector<uint8_t>> get_firmware_version();
@@ -77,6 +80,7 @@ public:
void set_power_saving(bool power_saving);
void enable_deepsleep();
void send_heartbeat(bool engaged);
bool set_aol_axis_request(uint8_t axis_mask);
void set_can_speed_kbps(uint16_t bus, uint16_t speed);
void set_can_fd_auto(uint16_t bus, bool enabled);
void set_data_speed_kbps(uint16_t bus, uint16_t speed);
@@ -90,7 +94,9 @@ protected:
uint8_t receive_buffer[RECV_SIZE + sizeof(can_header) + 64];
uint32_t receive_buffer_size = 0;
Panda() {}
Panda(uint32_t bus_offset=0) : bus_offset(bus_offset) {}
Panda(std::unique_ptr<PandaCommsHandle> comms, cereal::PandaState::PandaType type, uint32_t offset=0)
: handle(std::move(comms)), bus_offset(offset), hw_type(type) {}
void pack_can_buffer(const capnp::List<cereal::CanData>::Reader &can_data_list,
std::function<void(uint8_t *, size_t)> write_func);
bool unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector<can_frame> &out_vec);
@@ -0,0 +1,257 @@
#include "selfdrive/pandad/aol_protocol.h"
#include <cassert>
#include <cstring>
#include <deque>
#include <vector>
namespace {
constexpr uint8_t TEST_MODE = 7U;
constexpr uint8_t TEST_ALT_MODE = 8U;
constexpr uint16_t TEST_PARAM = 0x1234U;
constexpr uint64_t NOW = 1000000000ULL;
bool test_param(uint16_t param) { return param == TEST_PARAM; }
bool test_alt_param(uint16_t param) { return param == 0x2345U; }
constexpr AolSafetyProfile TEST_PROFILES[] = {
{TEST_MODE, test_param, false}, {TEST_ALT_MODE, test_alt_param, false},
};
const AolProfileRegistry TEST_REGISTRY{TEST_PROFILES, std::size(TEST_PROFILES)};
aol_safety_health_t status(uint8_t request = 0U, uint8_t permission = 0U) {
return {AOL_SAFETY_PROTOCOL_MAGIC, AOL_SAFETY_PROTOCOL_VERSION, request, permission,
TEST_MODE, TEST_PARAM, 0x1U};
}
std::vector<unsigned char> bytes(const aol_safety_health_t &s) {
std::vector<unsigned char> raw(sizeof(s));
std::memcpy(raw.data(), &s, sizeof(s));
return raw;
}
struct FakeTransport {
std::deque<std::vector<unsigned char>> replies;
bool write_ok = true;
bool wrote = false;
uint8_t last_write = 0xffU;
std::optional<aol_safety_health_t> read() {
if (replies.empty()) return std::nullopt;
auto raw = replies.front();
replies.pop_front();
return parse_aol_status(raw.data(), static_cast<int>(raw.size()));
}
bool write(uint8_t request) {
wrote = true;
last_write = request;
return write_ok;
}
};
struct Step {
AolWritePlan plan;
AolOutcome outcome;
};
Step run(AolAxisNegotiator &negotiator, FakeTransport &transport, const AolAxisInput &axis,
uint64_t now_ns = NOW, uint8_t expected_mode = TEST_MODE) {
auto before = transport.read();
auto plan = negotiator.prepare(before, axis, now_ns, expected_mode);
bool write_ok = plan.capable && transport.write(plan.request_mask);
auto after = write_ok ? transport.read() : std::nullopt;
return {plan, negotiator.complete(plan, write_ok, after)};
}
AolAxisInput axis(const char *session = "drive-1", bool lat = true, bool lon = false) {
return {true, true, session, NOW - 1000000ULL, NOW + 100000000ULL,
NOW - 1000000ULL, lat, lon};
}
void queue(FakeTransport &transport, const aol_safety_health_t &before,
const aol_safety_health_t &after) {
transport.replies.push_back(bytes(before));
transport.replies.push_back(bytes(after));
}
} // namespace
int main() {
static_assert(sizeof(aol_safety_health_t) == 11U);
static_assert(offsetof(aol_safety_health_t, safety_param) == 8U);
static_assert(offsetof(aol_safety_health_t, capability_flags) == 10U);
const uint64_t sampled_mono = aol_monotonic_ns();
assert(sampled_mono > 0U && aol_monotonic_ns() >= sampled_mono);
auto valid = status();
auto valid_bytes = bytes(valid);
assert(!parse_aol_status(nullptr, 0));
assert(!parse_aol_status(valid_bytes.data(), static_cast<int>(valid_bytes.size()) - 1));
valid.magic = 0;
auto wrong_magic = bytes(valid);
assert(!parse_aol_status(wrong_magic.data(), static_cast<int>(wrong_magic.size())));
valid = status();
valid.version = AOL_SAFETY_PROTOCOL_VERSION + 1U;
auto wrong_version = bytes(valid);
assert(!parse_aol_status(wrong_version.data(), static_cast<int>(wrong_version.size())));
auto unsupported = status();
const AolProfileRegistry empty_registry{nullptr, 0U};
assert(!aol_capable(unsupported, TEST_MODE, empty_registry));
assert(!aol_runtime_enabled(false, unsupported, empty_registry));
assert(aol_runtime_enabled(true, unsupported, empty_registry));
const AolProfileRegistry null_storage{nullptr, 1U};
assert(!aol_capable(unsupported, TEST_MODE, null_storage));
const AolSafetyProfile null_predicate{TEST_MODE, nullptr, true};
const AolProfileRegistry null_predicate_registry{&null_predicate, 1U};
assert(!aol_capable(unsupported, TEST_MODE, null_predicate_registry));
assert(!aol_runtime_enabled(false, unsupported, null_predicate_registry));
unsupported.capability_flags = 0U; // RELEASE/old firmware
assert(!aol_capable(unsupported, TEST_MODE, TEST_REGISTRY));
unsupported = status();
unsupported.safety_mode = 1U;
assert(!aol_capable(unsupported, TEST_MODE, TEST_REGISTRY));
unsupported = status();
unsupported.safety_param ^= 0x1U;
assert(!aol_capable(unsupported, TEST_MODE, TEST_REGISTRY));
AolAxisNegotiator negotiator(TEST_REGISTRY);
AolAxisNegotiator temporary_registry_negotiator(AolProfileRegistry{TEST_PROFILES, std::size(TEST_PROFILES)});
FakeTransport temporary_registry_transport;
queue(temporary_registry_transport, status(), status());
const auto temporary_registry_result = run(temporary_registry_negotiator, temporary_registry_transport, axis());
assert(temporary_registry_result.plan.capable && temporary_registry_result.outcome.compatible);
FakeTransport t;
t.replies.push_back({}); // absent/short first reply cannot authorize a write
auto result = run(negotiator, t, axis());
assert(!result.plan.capable && !result.outcome.compatible && !t.wrote);
queue(t, status(0x3U, 0x3U), status()); // new process flushes old request first
result = run(negotiator, t, axis());
assert(result.plan.capable && t.last_write == 0U && result.outcome.compatible);
assert(result.outcome.session == "drive-1");
queue(t, status(), status(0x1U, 0x1U));
result = run(negotiator, t, axis());
assert(t.last_write == 0x1U && result.outcome.status->request_mask == 0x1U);
queue(t, status(0x1U, 0x1U), status(0x1U, 0x1U));
result = run(negotiator, t, axis("drive-1", true, true));
assert(t.last_write == 0x3U && result.outcome.compatible);
assert(result.outcome.status->request_mask == 0x1U); // old long ack; lat remains usable
queue(t, status(0x3U, 0x3U), status(0x3U, 0x3U));
result = run(negotiator, t, axis("drive-1", false, true));
assert(t.last_write == 0x2U && result.outcome.status->request_mask == 0x3U);
queue(t, status(0x3U, 0x3U), status());
result = run(negotiator, t, axis("drive-2"));
assert(t.last_write == 0U && result.outcome.session == "drive-2");
queue(t, status(), status(0x1U, 0x1U));
result = run(negotiator, t, axis("drive-2"));
assert(t.last_write == 0x1U);
auto stale = axis("drive-2");
stale.valid_until_ns = NOW - 1U;
queue(t, status(0x1U, 0x1U), status());
result = run(negotiator, t, stale);
assert(t.last_write == 0U && result.outcome.status->request_mask == 0U);
t.replies.push_back(bytes(status()));
t.write_ok = false;
result = run(negotiator, t, axis("drive-3"));
assert(t.last_write == 0U && !result.outcome.compatible && result.outcome.session.empty());
t.write_ok = true;
queue(t, status(), status(0x1U, 0x1U)); // zero write without zero ack cannot finish reset
result = run(negotiator, t, axis("drive-3"));
assert(t.last_write == 0U && result.outcome.session.empty());
queue(t, status(), status());
result = run(negotiator, t, axis("drive-3"));
assert(t.last_write == 0U && result.outcome.session == "drive-3");
t.replies.push_back(bytes(status()));
t.replies.push_back({}); // failed second read invalidates this tick and resets
result = run(negotiator, t, axis("drive-3"));
assert(t.last_write == 0x1U && !result.outcome.compatible);
queue(t, status(), status());
result = run(negotiator, t, axis("drive-3"));
assert(t.last_write == 0U && result.outcome.session == "drive-3");
auto wrong_identity = status();
wrong_identity.safety_mode = 1U;
queue(t, status(), wrong_identity); // safety identity changed after the write
result = run(negotiator, t, axis("drive-3"));
assert(t.last_write == 0x1U && !result.outcome.compatible);
queue(t, status(), status());
result = run(negotiator, t, axis("drive-3"));
assert(t.last_write == 0U && result.outcome.session == "drive-3");
AolAxisNegotiator mode_switch_negotiator(TEST_REGISTRY);
FakeTransport switch_transport;
queue(switch_transport, status(), status());
result = run(mode_switch_negotiator, switch_transport, axis("same-session"));
assert(switch_transport.last_write == 0U && result.outcome.compatible);
queue(switch_transport, status(), status(0x1U, 0x1U));
result = run(mode_switch_negotiator, switch_transport, axis("same-session"));
assert(switch_transport.last_write == 0x1U);
auto alternate = status();
alternate.safety_mode = TEST_ALT_MODE;
alternate.safety_param = 0x2345U;
queue(switch_transport, alternate, alternate);
result = run(mode_switch_negotiator, switch_transport, axis("same-session"), NOW, TEST_ALT_MODE);
assert(switch_transport.last_write == 0U && result.outcome.compatible);
// A fresh Python axis frame uses CLOCK_MONOTONIC. Treating BOOTTIME as the
// same clock after a nine-second suspend leaves the native request at zero.
AolAxisNegotiator mono_negotiator(TEST_REGISTRY);
FakeTransport mono_transport;
queue(mono_transport, status(), status());
result = run(mono_negotiator, mono_transport, axis("clock-session"), NOW);
assert(result.plan.request_mask == 0U && result.outcome.compatible);
queue(mono_transport, status(), status(0x1U, 0x1U));
result = run(mono_negotiator, mono_transport, axis("clock-session"), NOW + 1000000ULL);
assert(result.plan.request_mask == 0x1U);
queue(mono_transport, status(0x1U, 0x1U), status());
result = run(mono_negotiator, mono_transport, axis("clock-session"), NOW + 201000000ULL);
assert(result.plan.request_mask == 0U); // genuine stale frame still revoked
// Suspended axes keep only the already-negotiated physical arm. No actuator
// request is added, and a fresh session or stale frame cannot retain it.
AolAxisNegotiator paused_negotiator(TEST_REGISTRY);
FakeTransport paused_transport;
auto paused = axis("paused-session", false, false);
paused.retain_lateral_arm = true;
queue(paused_transport, status(), status());
result = run(paused_negotiator, paused_transport, paused);
assert(result.plan.request_mask == 0U && !result.plan.retain_lateral_arm);
for (int i = 0; i < 5; ++i) {
queue(paused_transport, status(), status());
result = run(paused_negotiator, paused_transport, paused);
assert(result.plan.request_mask == 0U && result.plan.retain_lateral_arm);
}
queue(paused_transport, status(), status(0x1U, 0x1U));
result = run(paused_negotiator, paused_transport, axis("paused-session"));
assert(result.plan.request_mask == 0x1U);
queue(paused_transport, status(0x1U, 0x1U), status());
result = run(paused_negotiator, paused_transport, paused, NOW + 201000000ULL);
assert(result.plan.request_mask == 0U && !result.plan.retain_lateral_arm);
paused.alive_valid = false;
queue(paused_transport, status(), status());
result = run(paused_negotiator, paused_transport, paused);
assert(result.plan.request_mask == 0U && !result.plan.retain_lateral_arm);
paused.alive_valid = true;
paused.session = "replacement-session";
queue(paused_transport, status(), status());
result = run(paused_negotiator, paused_transport, paused);
assert(result.plan.request_mask == 0U && !result.plan.retain_lateral_arm);
paused.retain_lateral_arm = false;
queue(paused_transport, status(), status());
result = run(paused_negotiator, paused_transport, paused);
assert(result.plan.request_mask == 0U && !result.plan.retain_lateral_arm);
AolAxisNegotiator boot_negotiator(TEST_REGISTRY);
FakeTransport boot_transport;
queue(boot_transport, status(), status());
result = run(boot_negotiator, boot_transport, axis("clock-session"), NOW + 9000000000ULL);
assert(result.plan.request_mask == 0U && result.outcome.compatible);
queue(boot_transport, status(), status());
result = run(boot_negotiator, boot_transport, axis("clock-session"), NOW + 9001000000ULL);
assert(result.plan.request_mask == 0U);
}
@@ -0,0 +1,115 @@
#include "selfdrive/pandad/aol_wire.h"
#include "openpilot/cereal/gen/cpp/log.capnp.h"
#include <cassert>
#include <cstdio>
#include <string>
static constexpr uint64_t now = 1000000000ULL;
int main() {
const AolSafetyWireFields fields = {1, true, 100, 200, 5, 34, true, false, true, false, "panda", "axis",
{now + 9000000000ULL, {{0, "panda", 5, 34, 0, true, false, false, 0}}}};
auto bytes = encode_aol_safety_wire(fields);
assert(!bytes.empty() && bytes.size() <= 512);
capnp::MallocMessageBuilder event_builder;
auto event = event_builder.initRoot<cereal::Event>();
event.setAolSafetyWire(kj::arrayPtr(bytes.data(), bytes.size()));
assert(event.isAolSafetyWire());
auto aligned = kj::heapArray<capnp::word>(bytes.size() / sizeof(capnp::word));
std::memcpy(aligned.begin(), bytes.data(), bytes.size());
capnp::FlatArrayMessageReader current(aligned.asPtr());
auto wire = current.getRoot<cereal::AolAxisState::SafetyWire>();
assert(wire.getVersion() == 2U && wire.getPandaSerial() == "panda");
assert(wire.getSourcePandaStatesMonoTime() == now + 9000000000ULL);
assert(wire.getPandaInventory().size() == 1U);
assert(wire.getPandaInventory()[0].getHardwareSerial() == "panda");
assert(wire.getPandaInventory()[0].getSafetyParam() == 34U);
// Historic exact v1 bytes remain readable after adding the custom fields.
const uint64_t legacy_words[] = {
0x0000000900000000ULL, 0x0002000400000000ULL, 0x0005000100010b01ULL,
0x0000000000000064ULL, 0x00000000000000c8ULL, 0x0000000000000022ULL,
0x0000003200000005ULL, 0x0000002a00000005ULL, 0x00000061646e6170ULL,
0x0000000073697861ULL
};
capnp::FlatArrayMessageReader legacy(kj::arrayPtr(reinterpret_cast<const capnp::word *>(legacy_words),
sizeof(legacy_words) / sizeof(capnp::word)));
auto old_wire = legacy.getRoot<cereal::AolAxisState::SafetyWire>();
assert(old_wire.getVersion() == 1U && old_wire.getPandaSerial() == "panda");
assert(old_wire.getSourcePandaStatesMonoTime() == 0U && old_wire.getPandaInventory().size() == 0U);
for (uint8_t value : bytes) std::printf("%02x", value);
std::printf("\n");
auto bad = fields;
bad.panda_serial = std::string(97, 'x');
assert(encode_aol_safety_wire(bad).empty());
bad = fields;
bad.valid_until_mono_time = 99;
assert(encode_aol_safety_wire(bad).empty());
bad = fields;
bad.inventory = {};
assert(encode_aol_safety_wire(bad).empty());
bad = fields;
bad.inventory.slots[0].hardware_serial = std::string(97, 'x');
assert(encode_aol_safety_wire(bad).empty());
bad = fields;
bad.inventory.slots.push_back({1, "auxiliary", 19, 0, 0, false, false, false, 0});
auto pair_bytes = encode_aol_safety_wire(bad);
assert(!pair_bytes.empty() && pair_bytes.size() <= 512U);
bad.inventory.slots.push_back({2, "third", 19, 0, 0, false, false, false, 0});
assert(encode_aol_safety_wire(bad).empty());
capnp::MallocMessageBuilder intent_builder;
auto intent = intent_builder.initRoot<cereal::AolAxisState::IntentWire>();
intent.setKind(2);
intent.setVersion(1);
intent.setProducerSessionId("card-session");
intent.setCarStateLogMonoTime(now - 10000000ULL);
intent.setObservedMonoTime(now - 10000000ULL);
intent.setValidUntilMonoTime(now + 100000000ULL);
intent.setSettingsQualified(true);
intent.setLateralArmed(true);
auto armed = [&](uint64_t message_ns = now - 10000000ULL, uint64_t source_ns = now - 10000000ULL) {
auto flat = capnp::messageToFlatArray(intent_builder);
return aol_armed_intent(flat.asBytes(), message_ns, source_ns, now);
};
assert(armed());
assert(armed(now - 10000000ULL, now - 20000000ULL)); // newest Card may precede the next axis publication
assert(!armed(0));
assert(!armed(now + 1));
assert(!armed(now - 31000000ULL));
assert(!armed(now - 10000000ULL, now + 1));
assert(!armed(now - 10000000ULL, now - 41000000ULL));
intent.setLateralArmed(false);
assert(!armed());
intent.setLateralArmed(true);
intent.setSettingsQualified(false);
assert(!armed());
intent.setSettingsQualified(true);
intent.setObservedMonoTime(now + 1);
assert(!armed());
intent.setObservedMonoTime(now - 31000000ULL);
assert(!armed());
intent.setObservedMonoTime(now - 10000000ULL);
intent.setValidUntilMonoTime(now - 1);
assert(!armed());
intent.setValidUntilMonoTime(now + 100000000ULL);
intent.setKind(1);
assert(!armed());
intent.setKind(2);
intent.setVersion(2);
assert(!armed());
intent.setVersion(1);
intent.setProducerSessionId("");
assert(!armed());
intent.setProducerSessionId(std::string(97, 'x'));
assert(!armed());
intent.setProducerSessionId("card-session");
auto flat = capnp::messageToFlatArray(intent_builder);
auto raw = flat.asBytes();
std::vector<capnp::byte> malformed(raw.begin(), raw.end());
malformed[0] = 1; // multiple segments are forbidden
assert(!aol_armed_intent(kj::arrayPtr(malformed.data(), malformed.size()), now - 10000000ULL, now - 10000000ULL, now));
malformed[0] = 0;
malformed[8] = 0xff; // malformed root pointer
assert(!aol_armed_intent(kj::arrayPtr(malformed.data(), malformed.size()), now - 10000000ULL, now - 10000000ULL, now));
return 0;
}
+43
View File
@@ -0,0 +1,43 @@
# Independent lateral and longitudinal control
AOL connects Card button intent, SelfdriveD axis decisions, Controls output,
driver monitoring and acknowledged Panda permissions. Either control axis can
remain active independently while its permission is valid.
The shared owner handles settings, gestures, transport, freshness and axis
arbitration. Vehicle adapters in `starpilot/car/<brand>/aol.py` own supported
CarParams, safety profiles and button assignments. `policy.py` defines the
contract; `vehicle.py` dispatches to registered adapters. Unknown or malformed
configurations receive no capability. Settings availability, intent admission
and ordinary runtime support are separate: a saved preference cannot grant
steering or longitudinal permission. Current adapters do not cover the entire
vehicle catalog.
Card publishes desired intent; SelfdriveD resolves the two desired axes; Controls
requires matching current native permission before actuating either axis.
Session changes, expired messages, invalid required CAN and the existing
actuator limits still apply. Lateral-only operation uses engaged driver
monitoring independently of the Always On DM preference.
Intent and axis messages have a 30 ms freshness bound; native acknowledgments
have a 200 ms bound. The host and pandad use the same monotonic clock. A new
session first negotiates an acknowledged zero-axis request. These checks are
independent of each vehicle's Panda safety policy.
`aolAxisState` uses custom slot 16. Intent and safety use reserved Data events 125
and 124 with bounded, versioned flat Cap'n Proto payloads. Mapd slots 17–19 remain
separate. Decode stored messages with their writing schema; never reinterpret
an incompatible cache or reuse an occupied event identifier. Upstream vehicle
schemas remain unchanged.
`AolBrakePauseSpeedMps` stores meters per second. If absent, `PauseAOLOnBrake`
retains its historical effective meters-per-second value, regardless of the old
UI label. An invalid threshold disables the additional AOL request without
rewriting the preference. Settings convert display units when writing the new
key. Invalid CAN or transfer of button ownership cancels an incomplete hold;
a deliberate axis pause remains latched until changed by the driver.
Common transport and arbitration tests live in `aol/tests`. Vehicle host tests
live in `selfdrive/car/tests`, and native safety tests in
`opendbc_repo/opendbc/safety/tests`. Keep new vehicle admission and its positive
and negative safety tests with the corresponding vehicle port.
+1
View File
@@ -0,0 +1 @@
"""Independent-axis intent and permission contracts."""
+221
View File
@@ -0,0 +1,221 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from opendbc.car.structs import car
from openpilot.cereal import log
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS
ButtonType = car.CarState.ButtonEvent.Type
GearShifter = car.CarState.GearShifter
AOL_TOGGLE = 9
PAUSE_LATERAL = 3
PAUSE_LONGITUDINAL = 4
@dataclass(frozen=True)
class AolSettings:
enabled: bool
pause_brake_mps: float
lkas_action: int
main_action: int
distance_actions: tuple[int, int, int]
cancel_actions: tuple[int, int, int]
def _button_action(params, key: str) -> int:
try:
value = int(params.get(key, return_default=True) or '0')
except (TypeError, ValueError):
return 0
return value if value in (0, PAUSE_LATERAL, PAUSE_LONGITUDINAL, AOL_TOGGLE) else 0
def read_settings(params) -> AolSettings:
# Legacy thresholds are effectively SI despite the mph label; preserve them until replaced.
# Read raw bytes so malformed overrides cannot silently restore AOL.
pause_mps = 0.0
for key in ('AolBrakePauseSpeedMps', 'PauseAOLOnBrake'):
try:
pause_mps = float(Path(params.get_param_path(key)).read_bytes())
except FileNotFoundError:
continue
except (OSError, ValueError):
pause_mps = float('nan')
break
valid_pause = 0.0 <= pause_mps <= 100.0
return AolSettings(
enabled=params.get_bool('AlwaysOnLateral') and valid_pause, pause_brake_mps=pause_mps if valid_pause else 0.0,
lkas_action=_button_action(params, 'LKASButtonControl'),
main_action=_button_action(params, 'MainCruiseButtonControl'),
distance_actions=(_button_action(params, 'DistanceButtonControl'),
_button_action(params, 'LongDistanceButtonControl'),
_button_action(params, 'VeryLongDistanceButtonControl')),
cancel_actions=(_button_action(params, 'CancelButtonControl'),
_button_action(params, 'LongCancelButtonControl'),
_button_action(params, 'VeryLongCancelButtonControl')),
)
def independent_axis_requested(settings: AolSettings) -> bool:
pause = (PAUSE_LATERAL, PAUSE_LONGITUDINAL)
return settings.enabled or any(action in pause for action in
(settings.lkas_action, settings.main_action, *settings.distance_actions))
def disarming_fault(events, CS) -> bool:
"""Temporary steering/known non-driving gears pause output, not driver intent."""
if CS.steerFaultPermanent:
return True
for event in events:
if event.immediateDisable:
return True
if event.softDisable:
if event.name == log.OnroadEvent.EventName.steerTempUnavailable:
continue
if event.name == log.OnroadEvent.EventName.wrongGear and CS.gearShifter != GearShifter.unknown:
continue
return True
return False
class AolCardIntent:
def __init__(self, settings: AolSettings, *, explicit_latch: bool = False):
self.settings = settings
self.explicit_latch = explicit_latch
self.allowed_latch = False
self.pause_lateral = False
self.pause_longitudinal = False
self._gesture_neutral_seen = False
self._distance_neutral_seen = False
self._main_held = False
self._lkas_held = False
self._fault_inhibit = False
self._last_latch_edge_ns = 0
self._native_rejection_ns = 0
self._timers = {int(ButtonType.gapAdjustCruise): 0}
self._held = {int(ButtonType.gapAdjustCruise): False}
def _perform(self, action: int) -> None:
if action == AOL_TOGGLE and self.settings.enabled and not self.explicit_latch:
self.allowed_latch = not self.allowed_latch
if self.allowed_latch:
self.pause_lateral = False
elif action == PAUSE_LATERAL:
self.pause_lateral = not self.pause_lateral
elif action == PAUSE_LONGITUDINAL:
self.pause_longitudinal = not self.pause_longitudinal
def update(self, CS, *, consumed_buttons: frozenset[int] = frozenset(), fault_active: bool | None = None,
now_ns: int = 0, native_rejection_ns: int = 0, standard_enabled: bool = False) -> None:
if self.explicit_latch and self._native_rejection_ns < native_rejection_ns <= now_ns:
self._native_rejection_ns = native_rejection_ns
if self._last_latch_edge_ns < native_rejection_ns <= now_ns:
# Native authorization was revoked. Keep neutral/held-button history so
# the next fresh physical press arms once, without replaying a held one.
self.allowed_latch = False
if self.explicit_latch and (fault_active is not None or CS.steerFaultPermanent):
self._fault_inhibit = bool(fault_active or CS.steerFaultPermanent)
if self._fault_inhibit:
self.allowed_latch = False
self._gesture_neutral_seen = False
if not CS.canValid or CS.canTimeout:
self.allowed_latch = False
self._gesture_neutral_seen = False
self._distance_neutral_seen = False
self._main_held = False
self._lkas_held = False
for button in self._held:
self._held[button] = False
self._timers[button] = 0
return
if not self.settings.enabled:
self.allowed_latch = False
if self.explicit_latch:
self._gesture_neutral_seen = False
if not independent_axis_requested(self.settings):
self.pause_lateral = False
self.pause_longitudinal = False
lkas_managed = self.settings.lkas_action == AOL_TOGGLE
main_managed = self.settings.main_action == AOL_TOGGLE
if self.explicit_latch and CS.accFaulted:
self.allowed_latch = False
self._gesture_neutral_seen = False
elif self.settings.enabled and not self.explicit_latch and not lkas_managed and not main_managed:
self.allowed_latch = bool(CS.cruiseState.available)
elif not self.explicit_latch and not CS.cruiseState.available:
self.allowed_latch = False
for button in consumed_buttons:
if button in self._held:
self._held[button] = False
self._timers[button] = 0
distance_events = [event for event in CS.buttonEvents if event.type in (ButtonType.gapAdjustCruise, ButtonType.lkas)]
if not self._distance_neutral_seen:
self._distance_neutral_seen = not any(event.pressed for event in distance_events)
if self.explicit_latch:
previously_held = self._main_held or self._lkas_held
cancel_pressed = False
for event in CS.buttonEvents:
button = int(event.type.raw)
if button == int(ButtonType.mainCruise):
self._main_held = bool(event.pressed)
elif button == int(ButtonType.lkas):
self._lkas_held = bool(event.pressed)
elif button == int(ButtonType.cancel) and event.pressed:
cancel_pressed = True
gesture_held = self._main_held or self._lkas_held
if not previously_held and gesture_held:
self._last_latch_edge_ns = now_ns
if cancel_pressed:
self.allowed_latch = False
elif not self._gesture_neutral_seen:
self._gesture_neutral_seen = not gesture_held
elif not previously_held and gesture_held and self.settings.enabled and not CS.accFaulted and not self._fault_inhibit:
# The physical gesture owns lateral intent even while ordinary cruise
# is engaged. Dropping only the AOL latch leaves standard steering on.
lateral_engaged = not self.pause_lateral and (self.allowed_latch or standard_enabled)
self.allowed_latch = not lateral_engaged
self.pause_lateral = lateral_engaged
for event in CS.buttonEvents:
button = int(event.type.raw)
if button in consumed_buttons:
continue
if self.explicit_latch and button in (int(ButtonType.cancel), int(ButtonType.lkas), int(ButtonType.mainCruise)):
continue
if button == int(ButtonType.gapAdjustCruise) and not self._distance_neutral_seen:
continue
if button == int(ButtonType.lkas) and event.pressed:
self._perform(self.settings.lkas_action)
elif button == int(ButtonType.mainCruise) and event.pressed:
self._perform(self.settings.main_action)
elif button in self._held:
if event.pressed:
self._held[button] = True
self._timers[button] = 0
else:
ticks = self._timers[button]
actions = self.settings.distance_actions if button == int(ButtonType.gapAdjustCruise) else self.settings.cancel_actions
if 0 < ticks < CRUISE_LONG_PRESS:
self._perform(actions[0])
self._held[button] = False
self._timers[button] = 0
for button, held in self._held.items():
if held:
self._timers[button] += 1
actions = self.settings.distance_actions if button == int(ButtonType.gapAdjustCruise) else self.settings.cancel_actions
if self._timers[button] == CRUISE_LONG_PRESS:
self._perform(actions[1])
elif self._timers[button] == CRUISE_LONG_PRESS * 5:
self._perform(actions[2])
def output(self, CS) -> tuple[bool, bool, bool]:
driving = CS.gearShifter not in (GearShifter.neutral, GearShifter.park, GearShifter.reverse, GearShifter.unknown)
return (self.allowed_latch and self.settings.enabled and CS.canValid and driving,
self.pause_lateral, self.pause_longitudinal)
+23
View File
@@ -0,0 +1,23 @@
"""Vehicle-provided AOL capabilities; the default grants no authority."""
from dataclasses import dataclass
@dataclass(frozen=True)
class AolVehiclePolicy:
intent_supported: bool = False
settings_supported: bool = False
runtime_supported: bool = False
normal_runtime_supported: bool = False
ordinary_axis_ack_required: bool = False
cruise_main_required: bool = True
explicit_latch: bool = False
distance_personality: bool = False
fixed_cruise_buttons: bool = False
lkas_button_supported: bool = True
physical_stalk_owner: bool = False
distance_pause_only: bool = False
paddle_pause: bool = False
safety_param_addition: int = 0
alternative_experience_addition: int = 0
full_axis_runtime_required: bool = False
@@ -0,0 +1,71 @@
"""Reserved Data transport rejects malformed or historically colliding payloads."""
import unittest
import struct
from unittest.mock import Mock, patch
from openpilot.cereal import custom, messaging
from openpilot.starpilot.aol.wire import (IntentState, SafetyState, MAX_WIRE_BYTES, decode_intent,
decode_safety, encode_intent, encode_safety)
class AolWireTests(unittest.TestCase):
def test_native_cpp_flat_fixture_decodes(self):
# Exact output of test_aol_wire.cc (the native encoder test asserts it too).
raw = bytes.fromhex('00000000090000000000000004000200010b0100010005006400000000000000' +
'c80000000000000022000000000000000500000032000000050000002a000000' +
'70616e64610000006178697300000000')
self.assertEqual(decode_safety(raw), SafetyState(1, True, 100, 200, 5, 34,
True, False, True, False, 'panda', 'axis'))
def test_roundtrip_immutable_and_bounded(self):
intent = IntentState('card', 3, 100, 100, 120, True, False, True, True)
safety = SafetyState(1, True, 100, 200, 5, 34, True, False, True, False, 'panda', 'axis')
self.assertEqual(decode_intent(encode_intent(intent)), intent)
self.assertEqual(decode_safety(encode_safety(safety)), safety)
decoded = decode_intent(encode_intent(intent))
self.assertIsNotNone(decoded)
assert decoded is not None
with self.assertRaises(AttributeError):
decoded.__setattr__('allowedLatch', False)
with self.assertRaises(ValueError):
encode_intent(IntentState('x' * 97, 3, 100, 100, 120, True, False, True, True))
def test_corrupt_wrong_kind_version_and_pointer_fail_closed(self):
valid = encode_intent(IntentState('card', 3, 100, 100, 120, True, False, True, True))
# Even a valid packed payload cannot trigger decompression on the flat path.
packed_payload = bytes.fromhex('100950040257010b010105016401c8012211053211052a1f70616e64610f61786973')
for bad in (b'', valid[:3], valid + b'X' * MAX_WIRE_BYTES, b'\xff' * 12,
packed_payload,
struct.pack('<II', 0, 0x7fffffff) + valid[8:],
struct.pack('<II', 1, 1) + valid[8:],
encode_safety(SafetyState(1, True, 100, 200, 5, 34, True, False, True, False, 'panda', 'axis'))):
with self.subTest(bad=str(bad)[:8]):
self.assertIsNone(decode_intent(bad))
self.assertIsNone(Mock(wraps=decode_intent)(512))
wrong_version = custom.AolAxisState.IntentWire.new_message(kind=2, version=2, producerSessionId='card')
self.assertIsNone(decode_intent(wrong_version.to_bytes()))
wrong_kind = custom.AolAxisState.SafetyWire.new_message(kind=2, version=1)
self.assertIsNone(decode_safety(wrong_kind.to_bytes()))
# Segment table agrees with length, but the root struct pointer is invalid.
corrupt_pointer = bytearray(valid)
corrupt_pointer[8:16] = b'\xff' * 8
self.assertIsNone(decode_intent(bytes(corrupt_pointer)))
# Invalid UTF-8 in a text pointer is rejected during immutable materialization.
invalid_utf8 = bytearray(valid)
invalid_utf8[valid.index(b'card')] = 0xff
self.assertIsNone(decode_intent(bytes(invalid_utf8)))
def test_historic_map_ordinals_never_select_aol(self):
old_extended = messaging.new_message('mapdExtendedOut')
old_input = messaging.new_message('mapdIn')
self.assertEqual(str(old_extended.which()), 'mapdExtendedOut')
self.assertEqual(str(old_input.which()), 'mapdIn')
self.assertNotEqual(old_extended.which(), 'aolSafetyWire')
self.assertNotEqual(old_input.which(), 'aolIntentWire')
self.assertEqual(int(custom.MapdExtendedOut.schema.node.id), 0xa30662f84033036c)
self.assertEqual(int(custom.MapdIn.schema.node.id), 0xc86a3d38d13eb3ef)
if __name__ == '__main__':
unittest.main()
+200
View File
@@ -0,0 +1,200 @@
"""Strict, bounded AOL payloads in the reserved Event Data envelopes.
The outer Event remains Data at ordinals 124/125. The inner payload is flat
Cap'n Proto with its own version and kind; no caller consumes a raw reader.
"""
from __future__ import annotations
from dataclasses import dataclass
import struct
import capnp
from openpilot.cereal import custom
SAFETY_SERVICE = 'aolSafetyWire'
INTENT_SERVICE = 'aolIntentWire'
SAFETY_KIND = 1
INTENT_KIND = 2
WIRE_VERSION = 1
MAX_WIRE_BYTES = 512
MAX_WIRE_WORDS = (MAX_WIRE_BYTES - 8) // 8
MAX_ID_BYTES = 96
SAFETY_INVENTORY_VERSION = 2
MAX_PANDA_SLOTS = 2
@dataclass(frozen=True)
class PandaSlot:
slotIndex: int
hardwareSerial: str
safetyModel: int
safetyParam: int
alternativeExperience: int
controlsAllowed: bool = False
safetyRxChecksInvalid: bool = False
heartbeatLost: bool = False
faults: int = 0
@dataclass(frozen=True)
class SafetyState:
protocolVersion: int
compatible: bool
observedMonoTime: int
validUntilMonoTime: int
safetyModel: int
safetyParam: int
lateralAllowed: bool
longitudinalAllowed: bool
requestedLateral: bool
requestedLongitudinal: bool
pandaSerial: str
axisSessionId: str
sourcePandaStatesMonoTime: int = 0
pandaInventory: tuple[PandaSlot, ...] = ()
@dataclass(frozen=True)
class IntentState:
producerSessionId: str
sequence: int
carStateLogMonoTime: int
observedMonoTime: int
validUntilMonoTime: int
allowedLatch: bool
pauseLateral: bool
pauseLongitudinal: bool
settingsQualified: bool
lateralArmed: bool = False
optionalSetRelease: bool = False
def _bounded_id(value: str) -> str:
if not isinstance(value, str) or len(value.encode('utf-8')) > MAX_ID_BYTES:
raise ValueError('AOL wire identifier is invalid')
return value
def _payload(raw_value, schema, kind: int, versions=(WIRE_VERSION,)):
if not isinstance(raw_value, (bytes, bytearray, memoryview)):
return None
byte_count = raw_value.nbytes if isinstance(raw_value, memoryview) else len(raw_value)
if not 16 <= byte_count <= MAX_WIRE_BYTES or byte_count % 8:
return None
try:
raw = bytes(raw_value)
if len(raw) != byte_count:
return None
segment_count_minus_one, segment_words = struct.unpack_from('<II', raw)
if segment_count_minus_one != 0 or segment_words > MAX_WIRE_WORDS or len(raw) != 8 + 8 * segment_words:
return None
with schema.from_bytes(raw, traversal_limit_in_words=MAX_WIRE_WORDS, nesting_limit=4) as decoded:
if int(decoded.kind) != kind or int(decoded.version) not in versions:
return None
return decoded.to_dict()
except (ValueError, OverflowError, RuntimeError, UnicodeDecodeError, capnp.KjException):
return None
def _memoized(decode):
# Consumers poll at 100 Hz while wires change at 10-100 Hz; results are frozen, so reuse the last exact-bytes decode.
last = [None, None]
def wrapper(raw):
key = bytes(raw) if isinstance(raw, (bytes, bytearray, memoryview)) else None
if key is None:
return decode(raw)
if last[0] != key:
last[0], last[1] = key, decode(key)
return last[1]
wrapper.__wrapped__ = decode
return wrapper
@_memoized
def decode_safety(raw: bytes) -> SafetyState | None:
value = _payload(raw, custom.AolAxisState.SafetyWire, SAFETY_KIND, (WIRE_VERSION, SAFETY_INVENTORY_VERSION))
if value is None:
return None
try:
source_ns = int(value.get('sourcePandaStatesMonoTime', 0))
inventory = tuple(PandaSlot(int(slot['slotIndex']), _bounded_id(str(slot.get('hardwareSerial', ''))),
int(slot['safetyModel']), int(slot['safetyParam']), int(slot['alternativeExperience']),
bool(slot['controlsAllowed']), bool(slot['safetyRxChecksInvalid']),
bool(slot['heartbeatLost']), int(slot['faults']))
for slot in value.get('pandaInventory', ()))
if (int(value['version']) == WIRE_VERSION and (source_ns or inventory) or
int(value['version']) == SAFETY_INVENTORY_VERSION and
(source_ns <= 0 or not 1 <= len(inventory) <= MAX_PANDA_SLOTS)):
return None
result = SafetyState(int(value['protocolVersion']), bool(value['compatible']), int(value['observedMonoTime']),
int(value['validUntilMonoTime']), int(value['safetyModel']), int(value['safetyParam']),
bool(value['lateralAllowed']), bool(value['longitudinalAllowed']), bool(value['requestedLateral']),
bool(value['requestedLongitudinal']), _bounded_id(str(value.get('pandaSerial', ''))),
_bounded_id(str(value.get('axisSessionId', ''))), source_ns, inventory)
if result.validUntilMonoTime < result.observedMonoTime:
return None
return result
except (KeyError, ValueError, OverflowError, RuntimeError, capnp.KjException):
return None
@_memoized
def decode_intent(raw: bytes) -> IntentState | None:
value = _payload(raw, custom.AolAxisState.IntentWire, INTENT_KIND)
if value is None:
return None
try:
result = IntentState(_bounded_id(str(value.get('producerSessionId', ''))), int(value['sequence']),
int(value['carStateLogMonoTime']), int(value['observedMonoTime']), int(value['validUntilMonoTime']),
bool(value['allowedLatch']), bool(value['pauseLateral']), bool(value['pauseLongitudinal']),
bool(value['settingsQualified']), bool(value.get('lateralArmed', False)), bool(value.get('optionalSetRelease', False)))
if result.validUntilMonoTime < result.observedMonoTime:
return None
return result
except (KeyError, ValueError, OverflowError, RuntimeError, capnp.KjException):
return None
def encode_intent(value: IntentState) -> bytes:
_bounded_id(value.producerSessionId)
message = custom.AolAxisState.IntentWire.new_message(
kind=INTENT_KIND, version=WIRE_VERSION, producerSessionId=value.producerSessionId,
sequence=value.sequence, carStateLogMonoTime=value.carStateLogMonoTime,
observedMonoTime=value.observedMonoTime, validUntilMonoTime=value.validUntilMonoTime,
allowedLatch=value.allowedLatch, pauseLateral=value.pauseLateral,
pauseLongitudinal=value.pauseLongitudinal, settingsQualified=value.settingsQualified,
lateralArmed=value.lateralArmed, optionalSetRelease=value.optionalSetRelease)
data = message.to_bytes()
if len(data) > MAX_WIRE_BYTES:
raise ValueError('AOL intent payload exceeds wire bound')
return data
def encode_safety(value: SafetyState) -> bytes:
_bounded_id(value.pandaSerial)
_bounded_id(value.axisSessionId)
inventory = value.pandaInventory
inventory_version = bool(inventory or value.sourcePandaStatesMonoTime)
if inventory_version and (value.sourcePandaStatesMonoTime <= 0 or not 1 <= len(inventory) <= MAX_PANDA_SLOTS):
raise ValueError('AOL Panda inventory is invalid')
for slot in inventory:
_bounded_id(slot.hardwareSerial)
message = custom.AolAxisState.SafetyWire.new_message(
kind=SAFETY_KIND, version=SAFETY_INVENTORY_VERSION if inventory_version else WIRE_VERSION,
protocolVersion=value.protocolVersion,
compatible=value.compatible, observedMonoTime=value.observedMonoTime,
validUntilMonoTime=value.validUntilMonoTime, safetyModel=value.safetyModel,
safetyParam=value.safetyParam, lateralAllowed=value.lateralAllowed,
longitudinalAllowed=value.longitudinalAllowed, requestedLateral=value.requestedLateral,
requestedLongitudinal=value.requestedLongitudinal, pandaSerial=value.pandaSerial,
axisSessionId=value.axisSessionId, sourcePandaStatesMonoTime=value.sourcePandaStatesMonoTime,
pandaInventory=[{'slotIndex': slot.slotIndex, 'hardwareSerial': slot.hardwareSerial, 'safetyModel': slot.safetyModel,
'safetyParam': slot.safetyParam, 'alternativeExperience': slot.alternativeExperience,
'controlsAllowed': slot.controlsAllowed, 'safetyRxChecksInvalid': slot.safetyRxChecksInvalid,
'heartbeatLost': slot.heartbeatLost, 'faults': slot.faults} for slot in inventory])
data = message.to_bytes()
if len(data) > MAX_WIRE_BYTES:
raise ValueError('AOL safety payload exceeds wire bound')
return data
View File
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "openpilot/selfdrive/pandad/aol_protocol.h"
static inline bool ford_aol_param(uint16_t param) {
bool result = false;
switch (param) {
case 8U: case 9U: case 10U: case 11U: case 12U: case 13U:
case 18U: case 19U: case 32U: case 33U: case 66U: case 67U:
result = true;
break;
default:
break;
}
return result;
}
inline constexpr AolSafetyProfile FORD_AOL_PROFILE{6U, ford_aol_param, true};
@@ -0,0 +1,9 @@
#pragma once
#include "selfdrive/pandad/aol_protocol.h"
inline bool mazda_aol_param(uint16_t param) {
return param == 0U;
}
inline constexpr AolSafetyProfile MAZDA_AOL_PROFILE{13U, mazda_aol_param, true};
@@ -0,0 +1,15 @@
#pragma once
#include "selfdrive/pandad/aol_protocol.h"
inline bool tesla_preap_aol_param(uint16_t param) {
return param == 0U;
}
inline constexpr AolSafetyProfile TESLA_PREAP_AOL_PROFILE{39U, tesla_preap_aol_param, true};
inline bool tesla_screen_aol_param(uint16_t param) {
return param == 512U || param == 513U || param == 1536U || param == 1537U;
}
inline constexpr AolSafetyProfile TESLA_SCREEN_AOL_PROFILE{10U, tesla_screen_aol_param, true};
@@ -0,0 +1,9 @@
#pragma once
#include "selfdrive/pandad/aol_protocol.h"
inline bool toyota_aol_param(uint16_t param) {
return (param == 73U) || (param == 585U) || (param == 4169U) || (param == 4681U);
}
inline constexpr AolSafetyProfile TOYOTA_AOL_PROFILE{2U, toyota_aol_param, true};