feat: Squash all min-features into full

This commit is contained in:
Rick Lan
2026-06-11 20:00:23 +08:00
parent ce6611af1f
commit d328bb844d
447 changed files with 108504 additions and 163 deletions
+3
View File
@@ -25,6 +25,9 @@ def main():
dirty=build_metadata.openpilot.is_dirty,
device=HARDWARE.get_device_type())
if params.get_bool("dp_dev_disable_connect"):
time.sleep(31536000) # a year
return
try:
while 1:
cloudlog.info("starting athena daemon")
+15 -1
View File
@@ -13,10 +13,13 @@ from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
import os
UNREGISTERED_DONGLE_ID = "UnregisteredDevice"
LITE = os.getenv("LITE") is not None
def is_registered_device() -> bool:
dongle = Params().get("DongleId")
return dongle not in (None, UNREGISTERED_DONGLE_ID)
@@ -51,16 +54,27 @@ def register(show_spinner=False) -> str | None:
spinner = Spinner()
spinner.update("registering device")
if LITE:
params.put("DongleId", UNREGISTERED_DONGLE_ID)
return UNREGISTERED_DONGLE_ID
# Block until we get the imei
serial = HARDWARE.get_serial()
start_time = time.monotonic()
imei1: str | None = None
imei2: str | None = None
skip_imei_count = 0
while imei1 is None and imei2 is None:
try:
imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1)
except Exception:
cloudlog.exception("Error getting imei, trying again...")
spinner.update(f"registering device - serial: {serial}, Error getting IMEI, trying {skip_imei_count}/30")
# rick - no imei = can't register = skip everything
if skip_imei_count > 30:
params.put("DongleId", UNREGISTERED_DONGLE_ID)
return UNREGISTERED_DONGLE_ID
skip_imei_count += 1
time.sleep(1)
if time.monotonic() - start_time > 60 and show_spinner:
@@ -96,7 +110,7 @@ def register(show_spinner=False) -> str | None:
if dongle_id:
params.put("DongleId", dongle_id, block=True)
set_offroad_alert("Offroad_UnregisteredHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC)
set_offroad_alert("Offroad_UnregisteredHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC and not os.getenv("LITE"))
return dongle_id
+1 -1
View File
@@ -4,7 +4,7 @@ libs = [common, messaging, visionipc]
if arch != "Darwin":
camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/spectra.cc',
'cameras/cdm.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc'])
'cameras/cdm.cc', 'sensors/ar0231.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc'])
env.Program('camerad', ['main.cc', camera_obj], LIBS=libs)
if GetOption("extras") and arch == "x86_64":
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -1046,8 +1046,10 @@ bool SpectraCamera::openSensor() {
};
// Figure out which sensor we have
// rick - added back for c3
if (!init_sensor_lambda(new OS04C10) &&
!init_sensor_lambda(new OX03C10)) {
!init_sensor_lambda(new OX03C10) &&
!init_sensor_lambda(new AR0231)) {
LOGE("** sensor %d FAILED bringup, disabling", cc.camera_num);
enabled = false;
return false;
+136
View File
@@ -0,0 +1,136 @@
#include <cassert>
#include <cmath>
#include "system/camerad/sensors/sensor.h"
namespace {
const size_t AR0231_REGISTERS_HEIGHT = 2;
// TODO: this extra height is universal and doesn't apply per camera
const size_t AR0231_STATS_HEIGHT = 2 + 8;
const float sensor_analog_gains_AR0231[] = {
1.0 / 8.0, 2.0 / 8.0, 2.0 / 7.0, 3.0 / 7.0, // 0, 1, 2, 3
3.0 / 6.0, 4.0 / 6.0, 4.0 / 5.0, 5.0 / 5.0, // 4, 5, 6, 7
5.0 / 4.0, 6.0 / 4.0, 6.0 / 3.0, 7.0 / 3.0, // 8, 9, 10, 11
7.0 / 2.0, 8.0 / 2.0, 8.0 / 1.0}; // 12, 13, 14, 15 = bypass
} // namespace
AR0231::AR0231() {
image_sensor = cereal::FrameData::ImageSensor::AR0231;
bayer_pattern = CAM_ISP_PATTERN_BAYER_GRGRGR;
pixel_size_mm = 0.003;
data_word = true;
frame_width = 1928;
frame_height = 1208;
frame_stride = (frame_width * 12 / 8) + 4;
extra_height = AR0231_REGISTERS_HEIGHT + AR0231_STATS_HEIGHT;
registers_offset = 0;
frame_offset = AR0231_REGISTERS_HEIGHT;
stats_offset = AR0231_REGISTERS_HEIGHT + frame_height;
start_reg_array.assign(std::begin(start_reg_array_ar0231), std::end(start_reg_array_ar0231));
init_reg_array.assign(std::begin(init_array_ar0231), std::end(init_array_ar0231));
probe_reg_addr = 0x3000;
probe_expected_data = 0x354;
bits_per_pixel = 12;
mipi_format = CAM_FORMAT_MIPI_RAW_12;
frame_data_type = 0x12; // Changing stats to 0x2C doesn't work, so change pixels to 0x12 instead
mclk_frequency = 19200000; //Hz
readout_time_ns = 22850000;
dc_gain_factor = 2.5;
dc_gain_min_weight = 0;
dc_gain_max_weight = 1;
dc_gain_on_grey = 0.2;
dc_gain_off_grey = 0.3;
exposure_time_min = 2; // with HDR, fastest ss
exposure_time_max = 0x0855; // with HDR, slowest ss, 40ms
analog_gain_min_idx = 0x1; // 0.25x
analog_gain_rec_idx = 0x6; // 0.8x
analog_gain_max_idx = 0xD; // 4.0x
analog_gain_cost_delta = 0;
analog_gain_cost_low = 0.1;
analog_gain_cost_high = 5.0;
for (int i = 0; i <= analog_gain_max_idx; i++) {
sensor_analog_gains[i] = sensor_analog_gains_AR0231[i];
}
min_ev = exposure_time_min * sensor_analog_gains[analog_gain_min_idx];
max_ev = exposure_time_max * dc_gain_factor * sensor_analog_gains[analog_gain_max_idx];
target_grey_factor = 1.0;
black_level = 168;
color_correct_matrix = {
0x000000af, 0x00000ff9, 0x00000fd8,
0x00000fbc, 0x000000bb, 0x00000009,
0x00000fb6, 0x00000fe0, 0x000000ea,
};
for (int i = 0; i < 65; i++) {
float fx = i / 64.0;
const float gamma_k = 0.75;
const float gamma_b = 0.125;
const float mp = 0.01; // ideally midpoint should be adaptive
const float rk = 9 - 100*mp;
// poly approximation for s curve
fx = (fx > mp) ?
((rk * (fx-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(fx-mp))) + gamma_k*mp + gamma_b) :
((rk * (fx-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(fx-mp))) + gamma_k*mp + gamma_b);
gamma_lut_rgb.push_back((uint32_t)(fx*1023.0 + 0.5));
}
prepare_gamma_lut();
linearization_lut = {
0x02000000, 0x02000000, 0x02000000, 0x02000000,
0x020007ff, 0x020007ff, 0x020007ff, 0x020007ff,
0x02000bff, 0x02000bff, 0x02000bff, 0x02000bff,
0x020017ff, 0x020017ff, 0x020017ff, 0x020017ff,
0x02001bff, 0x02001bff, 0x02001bff, 0x02001bff,
0x020023ff, 0x020023ff, 0x020023ff, 0x020023ff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
};
linearization_pts = {0x07ff0bff, 0x17ff1bff, 0x23ff3fff, 0x3fff3fff};
vignetting_lut = {
0x00eaa755, 0x00cf2679, 0x00bc05e0, 0x00acc566, 0x00a1450a, 0x009984cc, 0x0095a4ad, 0x009584ac, 0x009944ca, 0x00a0c506, 0x00ac0560, 0x00bb25d9, 0x00ce2671, 0x00e90748, 0x01112889, 0x014a2a51, 0x01984cc2,
0x00db06d8, 0x00c30618, 0x00afe57f, 0x00a0a505, 0x009524a9, 0x008d646b, 0x0089844c, 0x0089644b, 0x008d2469, 0x0094a4a5, 0x009fe4ff, 0x00af0578, 0x00c20610, 0x00d986cc, 0x00fda7ed, 0x01320990, 0x017aebd7,
0x00d1868c, 0x00baa5d5, 0x00a7853c, 0x009844c2, 0x008cc466, 0x0085a42d, 0x0083641b, 0x0083641b, 0x0085842c, 0x008c4462, 0x0097a4bd, 0x00a6c536, 0x00b9a5cd, 0x00d06683, 0x00f1678b, 0x01226913, 0x0167ab3d,
0x00cd0668, 0x00b625b1, 0x00a30518, 0x0093c49e, 0x00884442, 0x00830418, 0x0080e407, 0x0080c406, 0x0082e417, 0x0087c43e, 0x00932499, 0x00a22511, 0x00b525a9, 0x00cbe65f, 0x00eb0758, 0x011a68d3, 0x015daaed,
0x00cc4662, 0x00b565ab, 0x00a24512, 0x00930498, 0x0087843c, 0x0082a415, 0x00806403, 0x00806403, 0x00828414, 0x00870438, 0x00926493, 0x00a1850c, 0x00b465a3, 0x00cb2659, 0x00ea2751, 0x011928c9, 0x015c2ae1,
0x00cf667b, 0x00b885c4, 0x00a5652b, 0x009624b1, 0x008aa455, 0x00846423, 0x00822411, 0x00822411, 0x00844422, 0x008a2451, 0x009564ab, 0x00a48524, 0x00b785bc, 0x00ce4672, 0x00ee6773, 0x011e88f4, 0x0162eb17,
0x00d6c6b6, 0x00bf65fb, 0x00ac4562, 0x009d04e8, 0x0091848c, 0x0089c44e, 0x00862431, 0x00860430, 0x0089844c, 0x00910488, 0x009c64e3, 0x00ab655b, 0x00be65f3, 0x00d566ab, 0x00f847c2, 0x012b2959, 0x01726b93,
0x00e3e71f, 0x00ca0650, 0x00b705b8, 0x00a7a53d, 0x009c24e1, 0x009484a4, 0x00908484, 0x00908484, 0x009424a1, 0x009bc4de, 0x00a70538, 0x00b625b1, 0x00c90648, 0x00e26713, 0x0108e847, 0x013fe9ff, 0x018bcc5e,
0x00f807c0, 0x00d966cb, 0x00c5862c, 0x00b625b1, 0x00aaa555, 0x00a30518, 0x009f04f8, 0x009f04f8, 0x00a2a515, 0x00aa2551, 0x00b585ac, 0x00c4a625, 0x00d846c2, 0x00f647b2, 0x0121a90d, 0x015e4af2, 0x01b8cdc6,
0x011548aa, 0x00f1678b, 0x00d886c4, 0x00c86643, 0x00bce5e7, 0x00b545aa, 0x00b1658b, 0x00b1458a, 0x00b505a8, 0x00bc85e4, 0x00c7c63e, 0x00d786bc, 0x00efe77f, 0x0113489a, 0x0144ea27, 0x01888c44, 0x01fdcfee,
0x013e49f2, 0x0113e89f, 0x00f5a7ad, 0x00e0c706, 0x00d30698, 0x00cb665b, 0x00c7663b, 0x00c7663b, 0x00cb0658, 0x00d2a695, 0x00dfe6ff, 0x00f467a3, 0x01122891, 0x013be9df, 0x01750ba8, 0x01cfae7d, 0x025912c8,
0x01766bb3, 0x01446a23, 0x011fc8fe, 0x0105e82f, 0x00f467a3, 0x00e9874c, 0x00e46723, 0x00e44722, 0x00e92749, 0x00f3a79d, 0x0104c826, 0x011e48f2, 0x01424a12, 0x01738b9c, 0x01bf6dfb, 0x023611b0, 0x02ced676,
0x01cf8e7c, 0x01866c33, 0x015aaad5, 0x013ae9d7, 0x01250928, 0x011768bb, 0x0110a885, 0x01108884, 0x0116e8b7, 0x01242921, 0x0139a9cd, 0x0158eac7, 0x01840c20, 0x01cb0e58, 0x0233719b, 0x02b9d5ce, 0x03645b22,
};
}
std::vector<i2c_random_wr_payload> AR0231::getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const {
uint16_t analog_gain_reg = 0xFF00 | (new_exp_g << 4) | new_exp_g;
return {
{0x3366, analog_gain_reg},
{0x3362, (uint16_t)(dc_gain_enabled ? 0x1 : 0x0)},
{0x3012, (uint16_t)exposure_time},
};
}
int AR0231::getSlaveAddress(int port) const {
assert(port >= 0 && port <= 2);
return (int[]){0x20, 0x30, 0x20}[port];
}
float AR0231::getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const {
// Cost of ev diff
float score = std::abs(desired_ev - (exp_t * exp_gain)) * 10;
// Cost of absolute gain
float m = exp_g_idx > analog_gain_rec_idx ? analog_gain_cost_high : analog_gain_cost_low;
score += std::abs(exp_g_idx - (int)analog_gain_rec_idx) * m;
// Cost of changing gain
score += std::abs(exp_g_idx - gain_idx) * (score + 1.0) / 10.0;
return score;
}
+121
View File
@@ -0,0 +1,121 @@
#pragma once
const struct i2c_random_wr_payload start_reg_array_ar0231[] = {{0x301A, 0x91C}};
const struct i2c_random_wr_payload stop_reg_array_ar0231[] = {{0x301A, 0x918}};
const struct i2c_random_wr_payload init_array_ar0231[] = {
{0x301A, 0x0018}, // RESET_REGISTER
// **NOTE**: if this is changed, readout_time_ns must be updated in the Sensor config
// CLOCK Settings
// input clock is 19.2 / 2 * 0x37 = 528 MHz
// pixclk is 528 / 6 = 88 MHz
// full roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*FRAME_LENGTH_LINES)) = 39.99 ms
// img roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*Y_OUTPUT_CONTROL)) = 22.85 ms
{0x302A, 0x0006}, // VT_PIX_CLK_DIV
{0x302C, 0x0001}, // VT_SYS_CLK_DIV
{0x302E, 0x0002}, // PRE_PLL_CLK_DIV
{0x3030, 0x0037}, // PLL_MULTIPLIER
{0x3036, 0x000C}, // OP_PIX_CLK_DIV
{0x3038, 0x0001}, // OP_SYS_CLK_DIV
// FORMAT
{0x3040, 0xC000}, // READ_MODE
{0x3004, 0x0000}, // X_ADDR_START_
{0x3008, 0x0787}, // X_ADDR_END_
{0x3002, 0x0000}, // Y_ADDR_START_
{0x3006, 0x04B7}, // Y_ADDR_END_
{0x3032, 0x0000}, // SCALING_MODE
{0x30A2, 0x0001}, // X_ODD_INC_
{0x30A6, 0x0001}, // Y_ODD_INC_
{0x3402, 0x0788}, // X_OUTPUT_CONTROL
{0x3404, 0x04B8}, // Y_OUTPUT_CONTROL
{0x3064, 0x1982}, // SMIA_TEST
{0x30BA, 0x11F2}, // DIGITAL_CTRL
// Enable external trigger and disable GPIO outputs
{0x30CE, 0x0120}, // SLAVE_SH_SYNC_MODE | FRAME_START_MODE
{0x340A, 0xE0}, // GPIO3_INPUT_DISABLE | GPIO2_INPUT_DISABLE | GPIO1_INPUT_DISABLE
{0x340C, 0x802}, // GPIO_HIDRV_EN | GPIO0_ISEL=2
// Readout timing
{0x300C, 0x0672}, // LINE_LENGTH_PCK (valid for 3-exposure HDR)
{0x300A, 0x0855}, // FRAME_LENGTH_LINES
{0x3042, 0x0000}, // EXTRA_DELAY
// Readout Settings
{0x31AE, 0x0204}, // SERIAL_FORMAT, 4-lane MIPI
{0x31AC, 0x0C0C}, // DATA_FORMAT_BITS, 12 -> 12
{0x3342, 0x1212}, // MIPI_F1_PDT_EDT
{0x3346, 0x1212}, // MIPI_F2_PDT_EDT
{0x334A, 0x1212}, // MIPI_F3_PDT_EDT
{0x334E, 0x1212}, // MIPI_F4_PDT_EDT
{0x3344, 0x0011}, // MIPI_F1_VDT_VC
{0x3348, 0x0111}, // MIPI_F2_VDT_VC
{0x334C, 0x0211}, // MIPI_F3_VDT_VC
{0x3350, 0x0311}, // MIPI_F4_VDT_VC
{0x31B0, 0x0053}, // FRAME_PREAMBLE
{0x31B2, 0x003B}, // LINE_PREAMBLE
{0x301A, 0x001C}, // RESET_REGISTER
// Noise Corrections
{0x3092, 0x0C24}, // ROW_NOISE_CONTROL
{0x337A, 0x0C80}, // DBLC_SCALE0
{0x3370, 0x03B1}, // DBLC
{0x3044, 0x0400}, // DARK_CONTROL
// Enable temperature sensor
{0x30B4, 0x0007}, // TEMPSENS0_CTRL_REG
{0x30B8, 0x0007}, // TEMPSENS1_CTRL_REG
// Enable dead pixel correction using
// the 1D line correction scheme
{0x31E0, 0x0003},
// HDR Settings
{0x3082, 0x0004}, // OPERATION_MODE_CTRL
{0x3238, 0x0444}, // EXPOSURE_RATIO
{0x1008, 0x0361}, // FINE_INTEGRATION_TIME_MIN
{0x100C, 0x0589}, // FINE_INTEGRATION_TIME2_MIN
{0x100E, 0x07B1}, // FINE_INTEGRATION_TIME3_MIN
{0x1010, 0x0139}, // FINE_INTEGRATION_TIME4_MIN
// TODO: do these have to be lower than LINE_LENGTH_PCK?
{0x3014, 0x08CB}, // FINE_INTEGRATION_TIME_
{0x321E, 0x0894}, // FINE_INTEGRATION_TIME2
{0x31D0, 0x0000}, // COMPANDING, no good in 10 bit?
{0x33DA, 0x0000}, // COMPANDING
{0x318E, 0x0200}, // PRE_HDR_GAIN_EN
// DLO Settings
{0x3100, 0x4000}, // DLO_CONTROL0
{0x3280, 0x0CCC}, // T1 G1
{0x3282, 0x0CCC}, // T1 R
{0x3284, 0x0CCC}, // T1 B
{0x3286, 0x0CCC}, // T1 G2
{0x3288, 0x0FA0}, // T2 G1
{0x328A, 0x0FA0}, // T2 R
{0x328C, 0x0FA0}, // T2 B
{0x328E, 0x0FA0}, // T2 G2
// Initial Gains
{0x3022, 0x0001}, // GROUPED_PARAMETER_HOLD_
{0x3366, 0xFF77}, // ANALOG_GAIN (1x)
{0x3060, 0x3333}, // ANALOG_COLOR_GAIN
{0x3362, 0x0000}, // DC GAIN
{0x305A, 0x00F8}, // red gain
{0x3058, 0x0122}, // blue gain
{0x3056, 0x009A}, // g1 gain
{0x305C, 0x009A}, // g2 gain
{0x3022, 0x0000}, // GROUPED_PARAMETER_HOLD_
// Initial Integration Time
{0x3012, 0x0005},
};
+14
View File
@@ -12,6 +12,8 @@
#include "cereal/gen/cpp/log.capnp.h"
#include "system/camerad/sensors/ox03c10_registers.h"
#include "system/camerad/sensors/os04c10_registers.h"
// rick - for c3
#include "system/camerad/sensors/ar0231_registers.h"
#define ANALOG_GAIN_MAX_CNT 55
@@ -102,3 +104,15 @@ public:
float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override;
int getSlaveAddress(int port) const override;
};
// rick - for c3
class AR0231 : public SensorInfo {
public:
AR0231();
std::vector<i2c_random_wr_payload> getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const override;
float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override;
int getSlaveAddress(int port) const override;
private:
mutable std::map<uint16_t, std::pair<int, int>> ar0231_register_lut;
};
+9 -3
View File
@@ -35,6 +35,8 @@ DISCONNECT_TIMEOUT = 5. # wait 5 seconds before going offroad after disconnect
PANDA_STATES_TIMEOUT = round(1000 / SERVICE_LIST['pandaStates'].frequency * 1.5) # 1.5x the expected pandaState frequency
ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycle
LITE = os.getenv("LITE")
ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp'])
HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats',
'network_metered', 'modem_temps'])
@@ -198,6 +200,8 @@ def hardware_thread(end_event, hw_queue) -> None:
fan_controller = FanController(int(1./DT_HW))
dp_dev_go_off_road = False
while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT)
@@ -313,13 +317,15 @@ def hardware_thread(end_event, hw_queue) -> None:
msg.deviceState.fanSpeedPercentDesired = 100
# *** registration check ***
if not PC:
# if not PC:
# we enforce this for our software, but you are welcome
# to make a different decision in your software
startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID)
# startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID)
# Handle offroad/onroad transition
should_start = all(onroad_conditions.values())
if count % 6 == 0:
dp_dev_go_off_road = params.get_bool("dp_dev_go_off_road")
should_start = not dp_dev_go_off_road and all(onroad_conditions.values())
if started_ts is None:
should_start = should_start and all(startup_conditions.values())
+4
View File
@@ -28,6 +28,8 @@ class PowerMonitoring:
self.car_voltage_mV = 12e3 # Low-passed version of peripheralState voltage
self.car_voltage_instant_mV = 12e3 # Last value of peripheralState voltage
self.integration_lock = threading.Lock()
self.dp_dev_auto_shutdown_in = int(self.params.get("dp_dev_auto_shutdown_in") or -5) * 60
self.dp_dev_auto_shutdown = self.dp_dev_auto_shutdown_in >= 0
car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") or 0
@@ -112,6 +114,8 @@ class PowerMonitoring:
now = time.monotonic()
should_shutdown = False
offroad_time = (now - offroad_timestamp)
if started_seen and self.dp_dev_auto_shutdown and offroad_time > self.dp_dev_auto_shutdown_in:
return True
low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
should_shutdown |= offroad_time > MAX_TIME_OFFROAD_S
+30
View File
@@ -259,6 +259,7 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo
def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
update = json.load(open(manifest_path))
update = restore_partitions(update)
for partition in update:
if not partition.get('full_check', False):
clear_partition_hash(target_slot_number, partition)
@@ -274,6 +275,7 @@ def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None:
update = json.load(open(manifest_path))
update = restore_partitions(update)
cloudlog.info(f"Target slot {target_slot_number}")
@@ -303,8 +305,36 @@ def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, st
def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool:
update = json.load(open(manifest_path))
update = restore_partitions(update)
return all(verify_partition(target_slot_number, partition) for partition in update)
# Implementation by Rick
# This approach differs from common solutions and required extensive trial and error.
# If you reuse or adapt this function, please provide proper credit.
import base64
def restore_partitions(partitions):
with open(base64.b64decode("L3N5cy9maXJtd2FyZS9kZXZpY2V0cmVlL2Jhc2UvbW9kZWw=").decode('utf-8')) as f:
if f.read().strip('\x00').split('comma ')[-1] == 'tizi':
return partitions
partition_name_to_use = {'abl', 'boot'}
partitions_to_keep = {}
agnos_tici_path = ""
try:
encoded_path = "L2RhdGEvb3BlbnBpbG90L3N5c3RlbS9oYXJkd2FyZS90aWNpL2Fnbm9zX3RpY2kuanNvbg=="
agnos_tici_path = base64.b64decode(encoded_path).decode('utf-8')
with open(agnos_tici_path, 'r') as f:
tici_partitions = json.load(f)
partitions_to_keep = { p['name']: p for p in tici_partitions if p.get('name') in partition_name_to_use }
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Warning: Could not load TICI partition data from {agnos_tici_path}. Error: {e}")
return partitions
return [partitions_to_keep.get(p.get('name'), p) for p in partitions]
if __name__ == "__main__":
import argparse
+84
View File
@@ -0,0 +1,84 @@
[
{
"name": "xbl",
"url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz",
"hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
"hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
"size": 3282256,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89"
},
{
"name": "xbl_config",
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz",
"hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
"hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
"size": 98124,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f"
},
{
"name": "abl",
"url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz",
"hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
"size": 274432,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6"
},
{
"name": "aop",
"url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz",
"hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
"hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
"size": 184364,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180"
},
{
"name": "devcfg",
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz",
"hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
"hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
"size": 40336,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f"
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"size": 18515968,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
"size": 5368709120,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
"alt": {
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
"size": 5368709120
}
}
]
+35 -2
View File
@@ -7,6 +7,34 @@ from openpilot.common.i2c import SMBus
# https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf
AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask'])
EQParams = namedtuple('EQParams', ['K', 'k1', 'k2', 'c1', 'c2'])
def configs_from_eq_params(base, eq_params):
return [
AmpConfig("K (high)", (eq_params.K >> 8), base, 0, 0xFF),
AmpConfig("K (low)", (eq_params.K & 0xFF), base + 1, 0, 0xFF),
AmpConfig("k1 (high)", (eq_params.k1 >> 8), base + 2, 0, 0xFF),
AmpConfig("k1 (low)", (eq_params.k1 & 0xFF), base + 3, 0, 0xFF),
AmpConfig("k2 (high)", (eq_params.k2 >> 8), base + 4, 0, 0xFF),
AmpConfig("k2 (low)", (eq_params.k2 & 0xFF), base + 5, 0, 0xFF),
AmpConfig("c1 (high)", (eq_params.c1 >> 8), base + 6, 0, 0xFF),
AmpConfig("c1 (low)", (eq_params.c1 & 0xFF), base + 7, 0, 0xFF),
AmpConfig("c2 (high)", (eq_params.c2 >> 8), base + 8, 0, 0xFF),
AmpConfig("c2 (low)", (eq_params.c2 & 0xFF), base + 9, 0, 0xFF),
]
# tici amplifier EQ config (restored from openpilot v0.10.0)
TICI_CONFIG = [
AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111),
AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100),
AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111),
AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010),
*configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)),
*configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)),
*configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)),
*configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)),
*configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)),
]
CONFIG = [
AmpConfig("MCLK prescaler", 0b01, 0x10, 4, 0b00110000),
@@ -109,15 +137,20 @@ class Amplifier:
def set_global_shutdown(self, amp_disabled: bool) -> bool:
return self.set_configs([self._get_shutdown_config(amp_disabled), ])
def initialize_configuration(self) -> bool:
def initialize_configuration(self, model: str = "") -> bool:
cfgs = [
self._get_shutdown_config(True),
*CONFIG,
*(TICI_CONFIG if model == "tici" else []),
self._get_shutdown_config(False),
]
return self.set_configs(cfgs)
if __name__ == "__main__":
with open("/sys/firmware/devicetree/base/model") as f:
model = f.read().strip('\x00')
model = model.split('comma ')[-1]
amp = Amplifier()
amp.initialize_configuration()
amp.initialize_configuration(model)
+14 -6
View File
@@ -15,6 +15,8 @@ from openpilot.system.hardware.tici.lpa import TiciLPA
from openpilot.system.hardware.tici.pins import GPIO
from openpilot.system.hardware.tici.amplifier import Amplifier
LITE = os.getenv("LITE") is not None
NM = 'org.freedesktop.NetworkManager'
NM_CON_ACT = NM + '.Connection.Active'
NM_DEV = NM + '.Device'
@@ -64,7 +66,7 @@ class Tici(HardwareBase):
@cached_property
def amplifier(self):
if self.get_device_type() == "mici":
if self.get_device_type() == "mici" or LITE:
return None
return Amplifier()
@@ -102,7 +104,7 @@ class Tici(HardwareBase):
return int(f.read())
def set_ir_power(self, percent: int):
if self.get_device_type() == "tizi":
if self.get_device_type() in ("tici", "tizi"):
return
value = int((percent / 100) * 300)
@@ -162,7 +164,7 @@ class Tici(HardwareBase):
return self.get_modem_state().get('imei', '')
def get_network_info(self):
if self.get_device_type() == "mici":
if self.get_device_type() == "mici" or LITE:
return None
ms = self.get_modem_state()
@@ -230,6 +232,8 @@ class Tici(HardwareBase):
return self.get_modem_state().get('modem_version') or None
def get_modem_temperatures(self):
if LITE:
return []
return self.get_modem_state().get('temperatures', [])
def get_current_power_draw(self):
@@ -292,7 +296,7 @@ class Tici(HardwareBase):
if self.amplifier is not None:
self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled)
if not powersave_enabled:
self.amplifier.initialize_configuration()
self.amplifier.initialize_configuration(self.get_device_type())
# *** CPU config ***
@@ -330,7 +334,7 @@ class Tici(HardwareBase):
def initialize_hardware(self):
if self.amplifier is not None:
self.amplifier.initialize_configuration()
self.amplifier.initialize_configuration(self.get_device_type())
# Allow hardwared to write engagement status to kmsg
os.system("sudo chmod a+w /dev/kmsg")
@@ -372,11 +376,15 @@ class Tici(HardwareBase):
# pandad core
affine_irq(3, "spi_geni") # SPI
# rick - for c3
if "tici" in self.get_device_type():
affine_irq(3, "xhci-hcd:usb3") # aux panda USB (or potentially anything else on USB)
affine_irq(3, "xhci-hcd:usb1") # internal panda USB (also modem)
try:
pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip()
subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid])
subprocess.call(["sudo", "taskset", "-pc", "3", pid])
except subprocess.CalledProcessException as e:
except subprocess.CalledProcessError as e:
print(str(e))
def get_networks(self):
+169 -5
View File
@@ -170,6 +170,149 @@ class PPPSession:
subprocess.run(["sudo", "resolvectl", "revert", "ppp0"], capture_output=True)
# ============================================================================
# dragonpilot: QMI data path for the comma3 (tici) EG25
#
# The comma3 EG25 runs in QMI mode (AT+QCFG="usbnet"==0) and does NOT support
# *99# PPP data: pppd negotiates LCP/PAP then the modem drops at IPCP, so the
# PPP-only upstream daemon never gets an IP. QMISession is a drop-in for
# PPPSession (same interface the Modem state machine uses) that brings data up
# over QMI on wwan0 via qmicli. It is selected automatically in _do_initializing
# when the modem reports QMI mode; everything else (AT init, registration,
# polling, /dev/shm/modem) is unchanged. Kept self-contained to ease upstream
# modem.py merges.
# ============================================================================
QMI_DEV = "/dev/cdc-wdm0"
QMI_IFACE = "wwan0"
QMI_POLL_INTERVAL = 10.0 # s; how often to actually query the modem while connected (drop detection latency)
class QMISession:
"""Drop-in for PPPSession that connects over QMI/wwan0 instead of *99# PPP."""
MAX_FAILS = 3
IFACE = QMI_IFACE
def __init__(self):
self._fails = 0
self._ip = ""
self._last_status_check = 0.0
@staticmethod
def available() -> bool:
return os.path.exists(QMI_DEV)
@staticmethod
def _param(key):
try:
with open(f"/data/params/d/{key}") as f:
return f.read().strip()
except FileNotFoundError:
return ""
@staticmethod
def _qmicli(*args, timeout=30):
return subprocess.run(["sudo", "qmicli", "-d", QMI_DEV, "-p", *args],
capture_output=True, text=True, timeout=timeout)
def start(self):
apn = self._param("GsmApn")
# qmi_wwan needs raw-ip mode for IPv4; set it while the iface is down
subprocess.run(["sudo", "ip", "link", "set", QMI_IFACE, "down"], capture_output=True)
subprocess.run(["sudo", "sh", "-c", f"echo Y > /sys/class/net/{QMI_IFACE}/qmi/raw_ip"], capture_output=True)
subprocess.run(["sudo", "ip", "link", "set", QMI_IFACE, "up"], capture_output=True)
net = "ip-type=4" + (f",apn={apn}" if apn else "")
self._qmicli(f"--wds-start-network={net}", "--client-no-release-cid")
self._ip = ""
self._last_status_check = time.monotonic() # give the bearer an interval before the first drop check
logging.info(f"QMI start-network on {QMI_IFACE} (apn={apn or '(network-provided)'})")
def kill(self):
self.cleanup_routes()
subprocess.run(["sudo", "ip", "addr", "flush", "dev", QMI_IFACE], capture_output=True)
subprocess.run(["sudo", "ip", "link", "set", QMI_IFACE, "down"], capture_output=True)
self._ip = ""
@staticmethod
def reset_data_port():
pass # no DTR/serial data port for QMI
def has_exited(self) -> bool:
# Throttle: querying the modem every loop (1s) is wasteful and the link is stable.
# Between checks assume still up -> a drop is detected within QMI_POLL_INTERVAL.
now = time.monotonic()
if now - self._last_status_check < QMI_POLL_INTERVAL:
return False
self._last_status_check = now
r = self._qmicli("--wds-get-packet-service-status", timeout=10)
return "disconnected" in r.stdout.lower()
def reset_fail_counter(self):
self._fails = 0
def record_fail(self) -> bool:
self._fails += 1
return self._fails >= self.MAX_FAILS
@property
def fails(self) -> int:
return self._fails
def _settings(self):
r = self._qmicli("--wds-get-current-settings", timeout=10)
def grab(label):
for line in r.stdout.splitlines():
if label in line:
return line.split(":", 1)[1].strip()
return ""
dns = [d for d in (grab("IPv4 primary DNS"), grab("IPv4 secondary DNS")) if d]
return grab("IPv4 address"), grab("IPv4 gateway address"), grab("IPv4 subnet mask"), dns
@staticmethod
def cleanup_routes():
subprocess.run(["sudo", "ip", "route", "del", "default", "dev", QMI_IFACE], capture_output=True)
subprocess.run(["sudo", "ip", "route", "flush", "table", "1000"], capture_output=True)
while subprocess.run(["sudo", "ip", "rule", "del", "table", "1000"], capture_output=True).returncode == 0:
pass
subprocess.run(["sudo", "resolvectl", "revert", QMI_IFACE], capture_output=True)
def poll_iface(self) -> dict:
# The IP is stable for the whole session, so configure wwan0 once and then
# serve the cached value -- no qmicli per loop. _ip is cleared on (re)start.
if self._ip:
return {"ip_address": self._ip, "connected": True}
ip, gw, mask, dns = self._settings()
if not (ip and gw):
return {"connected": False, "ip_address": ""}
try:
IPv4Address(ip)
IPv4Address(gw)
except AddressValueError:
logging.warning(f"refusing route install with non-IPv4 ip={ip!r} gw={gw!r}")
return {}
prefix = sum(bin(int(o)).count("1") for o in mask.split(".")) if mask.count(".") == 3 else 32
self.cleanup_routes()
subprocess.run(["sudo", "ip", "addr", "flush", "dev", QMI_IFACE], capture_output=True)
cmds = [
["sudo", "ip", "addr", "add", f"{ip}/{prefix}", "dev", QMI_IFACE],
["sudo", "ip", "link", "set", QMI_IFACE, "up"],
["sudo", "ip", "route", "add", "default", "via", gw, "dev", QMI_IFACE, "metric", "1000"],
["sudo", "ip", "route", "add", "default", "via", gw, "dev", QMI_IFACE, "table", "1000"],
["sudo", "ip", "rule", "add", "from", ip, "table", "1000"],
]
for cmd in cmds:
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
logging.warning(f"qmi route install failed ({' '.join(cmd[1:])}): {r.stderr.strip()}")
self.cleanup_routes()
return {}
if dns:
subprocess.run(["sudo", "resolvectl", "dns", QMI_IFACE, *dns], capture_output=True)
subprocess.run(["sudo", "resolvectl", "default-route", QMI_IFACE, "yes"], capture_output=True)
self._ip = ip
logging.info(f"QMI {ip}/{prefix} via {gw} on {QMI_IFACE}, dns={dns}")
return {"ip_address": ip, "connected": True}
class Modem:
def __init__(self):
self._ppp = PPPSession()
@@ -263,11 +406,18 @@ class Modem:
cmds = [
# clear initial EPS bearer APN (some carriers reject the default)
'AT+CGDCONT=0,"IP",""',
]
# SIM hot swap
'AT+QSIMDET=1,0',
'AT+QSIMSTAT=1',
# SIM hot swap: skip on the comma3 (TICI_DOS) - matches openpilot v0.10.0, which
# only sent these on tizi (C3X). Enabling SIM-detect on the C3 can cause
# spurious SIM-removed events and drop the connection.
if "TICI_DOS" not in os.environ:
cmds += [
'AT+QSIMDET=1,0',
'AT+QSIMSTAT=1',
]
cmds += [
# configure modem as data-centric
'AT+QNVW=5280,0,"0102000000000000"',
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
@@ -276,6 +426,10 @@ class Modem:
for c in cmds:
self._at(c)
def _qmi_mode(self) -> bool:
# dragonpilot: comma3 (TICI_DOS) EG25 can't do *99# PPP -> use QMI on wwan0
return "TICI_DOS" in os.environ and QMISession.available()
def _do_initializing(self):
if not os.path.exists(AT_PORT):
return State.INITIALIZING
@@ -294,6 +448,13 @@ class Modem:
self._configure_modem(identity["modem_version"])
# dragonpilot: switch the data session to QMI once if the modem is QMI-mode (comma3 EG25)
if not isinstance(self._ppp, QMISession) and self._qmi_mode():
self._ppp.kill()
self._ppp.cleanup_routes()
self._ppp = QMISession()
logging.info("using QMI data path (wwan0)")
self.S.update(identity)
self._apn = self._read_param("GsmApn")
self._roaming_allowed = self._is_roaming_allowed()
@@ -470,6 +631,8 @@ class Modem:
return {}
def _poll_iface(self) -> dict:
if isinstance(self._ppp, QMISession): # dragonpilot: QMI handles its own iface/routes/dns
return self._ppp.poll_iface()
try:
r = subprocess.run(["ip", "-4", "addr", "show", "ppp0"], capture_output=True, text=True, timeout=2)
ip, peer = "", ""
@@ -509,10 +672,11 @@ class Modem:
return dns_servers
def _poll_byte_counters(self) -> dict:
iface = getattr(self._ppp, "IFACE", "ppp0") # dragonpilot: wwan0 under QMI, else ppp0
try:
with open("/sys/class/net/ppp0/statistics/tx_bytes") as f:
with open(f"/sys/class/net/{iface}/statistics/tx_bytes") as f:
tx = int(f.read().strip())
with open("/sys/class/net/ppp0/statistics/rx_bytes") as f:
with open(f"/sys/class/net/{iface}/statistics/rx_bytes") as f:
rx = int(f.read().strip())
except Exception:
return {}
+5
View File
@@ -25,3 +25,8 @@ class GPIO:
# Sensor interrupts
LSM_INT = 84
# rick - for c3
BMX055_ACCEL_INT = 21
BMX055_GYRO_INT = 23
BMX055_MAGN_INT = 87
+16
View File
@@ -5,6 +5,7 @@
#include <string>
#include <unordered_map>
#include <vector>
#include <set>
#include "common/params.h"
#include "system/loggerd/encoder/encoder.h"
@@ -230,8 +231,22 @@ void loggerd_thread() {
std::unique_ptr<Context> ctx(Context::create());
std::unique_ptr<Poller> poller(Poller::create());
const bool lite = getenv("LITE");
static const std::set<std::string> ignore_names = {
"driverCameraState",
"driverEncodeIdx",
"driverStateV2",
"driverMonitoringState",
"driverEncodeData",
// audio logs
"soundPressure",
"rawAudioData",
"audioFeedback",
};
// subscribe to all socks
for (const auto& [_, it] : services) {
if (lite && ignore_names.count(it.name)) continue;
const bool encoder = util::ends_with(it.name, "EncodeData");
const bool livestream_encoder = util::starts_with(it.name, "livestream");
const bool record_audio = (it.name == "rawAudioData") && Params().getBool("RecordAudio");
@@ -261,6 +276,7 @@ void loggerd_thread() {
std::vector<RemoteEncoder*> encoders_with_audio;
for (const auto &cam : cameras_logged) {
for (const auto &encoder_info : cam.encoder_infos) {
if (lite && ignore_names.count(encoder_info.publish_name)) continue;
encoder_infos_dict[encoder_info.publish_name] = encoder_info;
s.max_waiting++;
}
+43 -2
View File
@@ -20,6 +20,24 @@ from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_I
from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.version import get_build_metadata
from openpilot.system.hardware.hw import Paths
from openpilot.system.manager.vehicle_model_collector import VehicleModelCollector
# rick - dynamically import panda
import importlib
# Pre-register panda_main as panda before loading it
target_mod = "panda_tici" if "TICI_DOS" in os.environ else "panda"
print(f"panda dir: {target_mod}")
_mod = importlib.import_module(target_mod)
# 👇 Insert alias so "from panda import ..." inside panda_main works
sys.modules["panda"] = _mod
# Re-export everything
globals().update({k: v for k, v in _mod.__dict__.items() if not k.startswith("_")})
import time
def manager_init() -> None:
@@ -43,6 +61,7 @@ def manager_init() -> None:
default_value = params.get_default_value(k)
if default_value is not None and params.get(k) is None:
params.put(k, default_value, block=True)
params.put("dp_dev_model_list", VehicleModelCollector().get_json())
# Create folders needed for msgq
try:
@@ -68,7 +87,8 @@ def manager_init() -> None:
if reg_res:
dongle_id = reg_res
else:
raise Exception(f"Registration failed for device {serial}")
dongle_id = "UnregisteredDevice"
# raise Exception(f"Registration failed for device {serial}")
os.environ['DONGLE_ID'] = dongle_id # Needed for swaglog
os.environ['GIT_ORIGIN'] = build_metadata.openpilot.git_normalized_origin # Needed for swaglog
os.environ['GIT_BRANCH'] = build_metadata.channel # Needed for swaglog
@@ -125,6 +145,15 @@ def manager_thread() -> None:
ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=ignore)
started_prev = False
dp_dev_delay_time_started: float = 0.
dp_dev_delay_loggerd = int(params.get('dp_dev_delay_loggerd') or 0)
# Dictionary of processes to be delayed [process_name: delay_seconds]
dp_dev_delay_start_times: dict[str, float] = {
'loggerd': dp_dev_delay_loggerd,
'encoderd': dp_dev_delay_loggerd
}
ignition_prev = False
while True:
@@ -145,10 +174,22 @@ def manager_thread() -> None:
if started != started_prev:
write_onroad_params(started, params)
dp_ignore: list[str] = []
if started and not started_prev:
dp_dev_delay_time_started = time.monotonic()
elif not started and started_prev:
dp_dev_delay_time_started = 0.
if dp_dev_delay_time_started > 0.:
cur_time = time.monotonic()
for name, delay_time in dp_dev_delay_start_times.items():
if cur_time - dp_dev_delay_time_started < delay_time: # type: ignore
dp_ignore.append(name)
started_prev = started
ignition_prev = ignition
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore)
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=list(set(ignore) | set(dp_ignore)))
running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name)
for p in managed_processes.values() if p.proc)
+27 -7
View File
@@ -8,6 +8,9 @@ from openpilot.system.hardware import PC, TICI
from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess
WEBCAM = os.getenv("USE_WEBCAM") is not None
LITE = os.getenv("LITE") is not None
TICI_DOS = "TICI_DOS" in os.environ
def driverview(started: bool, params: Params, CP: car.CarParams) -> bool:
return started or params.get_bool("IsDriverViewEnabled")
@@ -46,9 +49,15 @@ def lat_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
def not_long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not params.get_bool("LongitudinalManeuverMode")
def opview(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("dp_dev_opview")
def qcomgps(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not ublox_available()
def beep(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("dp_dev_beep")
def always_run(started: bool, params: Params, CP: car.CarParams) -> bool:
return True
@@ -58,6 +67,12 @@ def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return not started
def dashy(started: bool, params: Params, CP: car.CarParams) -> bool:
return params.get_bool("dp_dev_dashy")
def comma_connect(started: bool, params: Params, CP: car.CarParams) -> bool:
return not params.get_bool("dp_dev_disable_connect")
def or_(*fns):
return lambda *args: operator.or_(*(fn(*args) for fn in fns))
@@ -69,14 +84,14 @@ procs = [
NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging),
NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad),
NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar),
NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], or_(notcar, opview)),
PythonProcess("logmessaged", "system.logmessaged", always_run),
NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM),
PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM),
PythonProcess("proclogd", "system.proclogd", only_onroad, enabled=platform.system() != "Darwin"),
PythonProcess("journald", "system.journald", only_onroad, platform.system() != "Darwin"),
PythonProcess("micd", "system.micd", iscar),
PythonProcess("micd", "system.micd", iscar, enabled=not LITE),
PythonProcess("timed", "system.timed", always_run, enabled=not PC),
PythonProcess("modeld", "selfdrive.modeld.modeld", only_onroad),
@@ -84,7 +99,8 @@ procs = [
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC),
PythonProcess("ui", "selfdrive.ui.ui", always_run, restart_if_crash=True),
PythonProcess("soundd", "selfdrive.ui.soundd", driverview),
PythonProcess("soundd", "selfdrive.ui.soundd", driverview, enabled=not LITE),
PythonProcess("beepd", "dragonpilot.selfdrive.ui.beepd", beep, enabled=TICI and LITE),
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),
PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad),
@@ -96,7 +112,7 @@ procs = [
PythonProcess("deleter", "system.loggerd.deleter", always_run),
PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(WEBCAM or not PC)),
PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI),
PythonProcess("pandad", "selfdrive.pandad.pandad", always_run),
PythonProcess("pandad", "selfdrive.pandad.pandad" if not TICI_DOS else "selfdrive.pandad_tici.pandad", always_run),
PythonProcess("paramsd", "selfdrive.locationd.paramsd", only_onroad),
PythonProcess("lagd", "selfdrive.locationd.lagd", only_onroad),
PythonProcess("ubloxd", "system.ubloxd.ubloxd", ublox, enabled=TICI),
@@ -106,18 +122,22 @@ procs = [
PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver),
PythonProcess("radard", "selfdrive.controls.radard", only_onroad),
PythonProcess("hardwared", "system.hardware.hardwared", always_run),
PythonProcess("modem", "system.hardware.tici.modem", always_run, enabled=TICI),
PythonProcess("modem", "system.hardware.tici.modem", always_run, enabled=TICI and not LITE),
PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC),
PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC),
PythonProcess("uploader", "system.loggerd.uploader", always_run),
PythonProcess("uploader", "system.loggerd.uploader", and_(comma_connect, always_run)),
PythonProcess("statsd", "system.statsd", always_run),
PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", only_onroad),
# debug procs
NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar),
PythonProcess("webrtcd", "system.webrtc.webrtcd", notcar),
PythonProcess("webrtcd", "system.webrtc.webrtcd", or_(notcar, opview)),
PythonProcess("webjoystick", "tools.bodyteleop.web", notcar),
PythonProcess("joystick", "tools.joystick.joystick_control", and_(joystick, iscar)),
# dashy
PythonProcess("serverd", "dragonpilot.dashy.serverd", always_run),
PythonProcess("dashyd", "dragonpilot.dashy.dashyd", and_(dashy, only_onroad)),
]
managed_processes = {p.name: p for p in procs}
+169
View File
@@ -0,0 +1,169 @@
"""
Copyright (c) 2025 Rick Lan
This software is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0).
You are free to share and adapt this work for non-commercial purposes, provided you give appropriate credit and distribute any modifications under the same license.
To view a copy of this license, visit:
http://creativecommons.org/licenses/by-nc-sa/4.0/
---
**Commercial Licensing:**
Use of this software for commercial purposes is strictly prohibited without a separate, paid license.
To purchase a commercial license, please contact ricklan@gmail.com.
"""
import os
import importlib
import json
from openpilot.common.basedir import BASEDIR
# from openpilot.common.params import Params
class VehicleModelCollector:
def __init__(self):
self.base_package = "opendbc.car"
self.base_path = f"{BASEDIR}/opendbc/car"
self.exclude_brands = ['body', 'mock']
# Define the lookup dictionary for brand-to-group mappings
self.brand_to_group_map = {
"chrysler": [
{"prefix": "DODGE_", "group": "Chrysler"},
{"prefix": "RAM_", "group": "Chrysler"},
{"prefix": "JEEP_", "group": "Chrysler"},
],
"gm": [
{"prefix": "BUICK_", "group": "GM"},
{"prefix": "CADILLAC_", "group": "GM"},
{"prefix": "CHEVROLET_", "group": "GM"},
{"prefix": "HOLDEN_", "group": "GM"},
],
"honda": {"prefix": "ACURA_", "group": "Honda"},
"toyota": {"prefix": "LEXUS_", "group": "Toyota"},
"hyundai": [
{"prefix": "KIA_", "group": "Hyundai"},
{"prefix": "GENESIS_", "group": "Hyundai"}
],
"volkswagen": [
{"prefix": "AUDI_", "group": "Volkswagen"},
{"prefix": "SKODA_", "group": "Volkswagen"},
{"prefix": "SEAT_", "group": "Volkswagen"}
]
}
# Define exceptions for group names
self.group_name_exceptions = {
"gm": "GM",
}
@staticmethod
def is_car_model(car_class, attr):
"""Check if the attribute is a car model (not callable and not a dunder attribute)"""
return not callable(getattr(car_class, attr)) and not attr.startswith("__")
@staticmethod
def move_to_proper_group(models, prefix):
"""
Moves models with a certain prefix to their respective group.
Example: Models starting with 'LEXUS_' should go to 'Lexus' group.
"""
moved_models = []
for model in models[:]: # Iterate over a copy to avoid modifying during iteration
if model.startswith(prefix):
moved_models.append(model)
models.remove(model) # Remove from the original group
return moved_models
def format_group_name(self, group_name):
"""
Formats group names according to the exceptions dictionary.
Groups in the exceptions dictionary are returned in all caps, others are title cased.
"""
return self.group_name_exceptions.get(group_name, group_name.title())
def collect_models(self):
"""Collect all car models and organize them by brand/group"""
# List all subdirectories (car brands)
car_brands = sorted([
name for name in os.listdir(self.base_path)
if os.path.isdir(os.path.join(self.base_path, name)) and not name.startswith("__")
])
grouped_models = {}
# Import CAR from each subdirectory and group models by brand
for brand in car_brands:
if brand in self.exclude_brands:
continue
module_name = f"{self.base_package}.{brand}.values"
try:
module = importlib.import_module(module_name)
if hasattr(module, "CAR"):
car_class = getattr(module, "CAR")
models = sorted([attr for attr in dir(car_class) if self.is_car_model(car_class, attr)])
# Check if the brand has a special group in the lookup map
if brand in self.brand_to_group_map:
group_info = self.brand_to_group_map[brand]
if isinstance(group_info, list): # If multiple prefixes for the brand
for prefix_info in group_info:
moved_models = self.move_to_proper_group(models, prefix_info["prefix"])
if moved_models:
if prefix_info["group"] not in grouped_models:
grouped_models[prefix_info["group"]] = []
grouped_models[prefix_info["group"]].extend(moved_models)
else: # Single prefix for the brand
moved_models = self.move_to_proper_group(models, group_info["prefix"])
if moved_models:
if group_info["group"] not in grouped_models:
grouped_models[group_info["group"]] = []
grouped_models[group_info["group"]].extend(moved_models)
# Add remaining models to the respective brand
if models:
grouped_models[brand] = models
except ModuleNotFoundError:
pass
# Merge groups that have the same formatted name (e.g., "chrysler" and "Chrysler")
merged_grouped_models = {}
for group_key, models_list in grouped_models.items():
formatted_group_name = self.format_group_name(group_key)
if formatted_group_name not in merged_grouped_models:
merged_grouped_models[formatted_group_name] = []
merged_grouped_models[formatted_group_name].extend(models_list)
# Create a new dictionary to hold the sorted models
sorted_models = {}
for group_key, models_list in merged_grouped_models.items():
sorted_models[group_key] = sorted(models_list)
return sorted_models
# def save_to_params(self, output=None):
# """Save the collected model list to Params"""
# if output is None:
# output = self.collect_models()
# Params().put("dp_dev_model_list", json.dumps(output))
# return output
#
# def run(self):
# """Collect models and save to params"""
# models = self.collect_models()
# self.save_to_params(models)
# return models
def get_json(self):
return self.collect_models()
def get(self):
return json.dumps(self.collect_models())
# Allow running as a script
if __name__ == "__main__":
collector = VehicleModelCollector()
print(collector.get())
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
Drift detector for the dashy openpilot-settings mirror.
We expose upstream openpilot's TogglesLayout + DeveloperLayout settings via the
dashy web UI by mirroring them into dragonpilot/settings/openpilot.*.py. This
test fails loudly when upstream adds or removes a toggle key — preventing the
mirror from silently going stale on the next openpilot version bump.
If upstream renames a key, add/remove the corresponding entry in the mirror file.
If upstream adds a brand-new toggle, decide whether to mirror it (most should)
and add a new entry. If upstream removes one, remove it from the mirror.
"""
import unittest
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from dragonpilot.settings import SETTINGS
# Upstream Developer toggles that ARE param-backed booleans (not buttons / SSH key widget).
# Update when upstream's DeveloperLayout changes.
EXPECTED_DEVELOPER_KEYS = {
"AdbEnabled", "SshEnabled", "JoystickDebugMode", "LongitudinalManeuverMode",
"AlphaLongitudinalEnabled", "ShowDebugInfo",
}
def _mirror_keys(section_title: str) -> set[str]:
for s in SETTINGS:
if s["title"] == section_title:
return {it["key"] for it in s["settings"]}
return set()
class TestOpenpilotMirror(unittest.TestCase):
def test_toggles_mirror_in_sync(self):
# TogglesLayout() reads params at __init__, so we instantiate it once.
upstream = set(TogglesLayout()._toggle_defs.keys())
# Plus LongitudinalPersonality (multi-button widget, exposed alongside toggles in upstream UI).
upstream.add("LongitudinalPersonality")
mirror = _mirror_keys("Openpilot")
missing = upstream - mirror
extra = mirror - upstream
self.assertFalse(missing or extra,
f"Openpilot mirror drift: missing={missing}, extra={extra}\n"
f"Update dragonpilot/settings/openpilot.toggles.py")
def test_developer_mirror_in_sync(self):
mirror = _mirror_keys("Developer")
missing = EXPECTED_DEVELOPER_KEYS - mirror
extra = mirror - EXPECTED_DEVELOPER_KEYS
self.assertFalse(missing or extra,
f"Developer mirror drift: missing={missing}, extra={extra}\n"
f"Update dragonpilot/settings/openpilot.developer.py and EXPECTED_DEVELOPER_KEYS here")
if __name__ == "__main__":
unittest.main()
+2 -1
View File
@@ -266,7 +266,8 @@ def init(pigeon: TTYPigeon) -> None:
set_power(False)
time.sleep(0.1)
set_power(True)
time.sleep(0.5)
# rick - make sleep twice long, give LITE more time.
time.sleep(1.0)
init_baudrate(pigeon)
init_pigeon(pigeon)
+1 -1
View File
@@ -22,7 +22,7 @@ from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.ui.lib.multilang import multilang
from openpilot.common.realtime import Ratekeeper
_DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20}.get(HARDWARE.get_device_type(), 60)))
_DEFAULT_FPS = int(os.getenv("FPS", {'tici': 20, 'tizi': 20}.get(HARDWARE.get_device_type(), 60)))
FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
+8
View File
@@ -154,6 +154,13 @@ class AdvancedNetworkSettings(Widget):
self._scroller = Scroller(items, line_separator=True, spacing=0)
# dp - retain tethering after reboot
# same logic as _toggle_tethering()
if self._params.get_bool("dp_dev_tethering"):
self._tethering_action.set_enabled(False)
self._wifi_metered_action.set_enabled(False)
self._wifi_manager.set_tethering_active(True)
def _on_network_updated(self, networks: list[Network]):
self._tethering_action.set_enabled(True)
self._tethering_action.set_state(self._wifi_manager.is_tethering_active())
@@ -169,6 +176,7 @@ class AdvancedNetworkSettings(Widget):
def _toggle_tethering(self):
checked = self._tethering_action.get_state()
self._params.put_bool_nonblocking("dp_dev_tethering", checked)
self._tethering_action.set_enabled(False)
if checked:
self._wifi_metered_action.set_enabled(False)