Split pandaState into peripheralState and pandaState (#22385)

* publish peripheralState from boardd

* refactor consumers

* rename thread

* peripheralState has panda type too

* add to process replay

* fix device build

* properly remove hardware unsupported alert

* latest peripheralState

* remove pandaState from thermal_monitor

* put that back

* add back harness check

* fix cloudlog

* needs pandaState

* Update selfdrive/thermald/tests/test_power_monitoring.py

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>

* dont conflate

* Update selfdrive/thermald/tests/test_power_monitoring.py

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>

* pigeon is peripheral

* cleanup

* less global

* comment

* move rtc to peripheral_control_thread

* better diff?

* whitespace

* get msg

* bump cereal

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
pull/22425/head
Willem Melching 2021-10-04 11:30:11 +02:00 committed by GitHub
parent 9c771b45fc
commit f4e2537b12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 232 additions and 207 deletions

2
cereal

@ -1 +1 @@
Subproject commit ce5ef7a1e5d16b3f6c91f4ac6ca20cba9f4a255c
Subproject commit 2881d47cea8b9ee0ba972f1515b13c13af0c0c57

View File

@ -40,6 +40,7 @@ std::atomic<bool> ignition(false);
ExitHandler do_exit;
std::string get_time_str(const struct tm &time) {
char s[30] = {'\0'};
std::strftime(s, std::size(s), "%Y-%m-%d %H:%M:%S", &time);
@ -143,7 +144,7 @@ Panda *usb_connect() {
// power on charging, only the first time. Panda can also change mode and it causes a brief disconneciton
#ifndef __x86_64__
static std::once_flag connected_once;
std::call_once(connected_once, &Panda::set_usb_power_mode, panda, cereal::PandaState::UsbPowerMode::CDP);
std::call_once(connected_once, &Panda::set_usb_power_mode, panda, cereal::PeripheralState::UsbPowerMode::CDP);
#endif
if (panda->has_rtc) {
@ -234,6 +235,13 @@ void can_recv_thread(Panda *panda) {
}
}
void send_empty_peripheral_state(PubMaster *pm) {
MessageBuilder msg;
auto peripheralState = msg.initEvent().initPeripheralState();
peripheralState.setPandaType(cereal::PandaState::PandaType::UNKNOWN);
pm->send("peripheralState", msg);
}
void send_empty_panda_state(PubMaster *pm) {
MessageBuilder msg;
auto pandaState = msg.initEvent().initPandaState();
@ -241,44 +249,113 @@ void send_empty_panda_state(PubMaster *pm) {
pm->send("pandaState", msg);
}
void panda_state_thread(PubMaster *pm, Panda *panda, bool spoofing_started) {
LOGD("start panda state thread");
uint32_t no_ignition_cnt = 0;
bool send_panda_state(PubMaster *pm, Panda *panda, bool spoofing_started) {
health_t pandaState = panda->get_state();
if (spoofing_started) {
pandaState.ignition_line = 1;
}
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
if (pandaState.safety_model == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT);
}
bool ignition = ((pandaState.ignition_line != 0) || (pandaState.ignition_can != 0));
#ifndef __x86_64__
bool power_save_desired = !ignition;
if (pandaState.power_save_enabled != power_save_desired) {
panda->set_power_saving(power_save_desired);
}
// set safety mode to NO_OUTPUT when car is off. ELM327 is an alternative if we want to leverage athenad/connect
if (!ignition && (pandaState.safety_model != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) {
panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT);
}
#endif
// build msg
MessageBuilder msg;
auto evt = msg.initEvent();
evt.setValid(panda->comms_healthy);
auto ps = evt.initPandaState();
ps.setUptime(pandaState.uptime);
ps.setIgnitionLine(pandaState.ignition_line);
ps.setIgnitionCan(pandaState.ignition_can);
ps.setControlsAllowed(pandaState.controls_allowed);
ps.setGasInterceptorDetected(pandaState.gas_interceptor_detected);
ps.setCanRxErrs(pandaState.can_rx_errs);
ps.setCanSendErrs(pandaState.can_send_errs);
ps.setCanFwdErrs(pandaState.can_fwd_errs);
ps.setGmlanSendErrs(pandaState.gmlan_send_errs);
ps.setPandaType(panda->hw_type);
ps.setSafetyModel(cereal::CarParams::SafetyModel(pandaState.safety_model));
ps.setSafetyParam(pandaState.safety_param);
ps.setFaultStatus(cereal::PandaState::FaultStatus(pandaState.fault_status));
ps.setPowerSaveEnabled((bool)(pandaState.power_save_enabled));
ps.setHeartbeatLost((bool)(pandaState.heartbeat_lost));
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(pandaState.car_harness_status));
// Convert faults bitset to capnp list
std::bitset<sizeof(pandaState.faults) * 8> fault_bits(pandaState.faults);
auto faults = ps.initFaults(fault_bits.count());
size_t i = 0;
for (size_t f = size_t(cereal::PandaState::FaultType::RELAY_MALFUNCTION);
f <= size_t(cereal::PandaState::FaultType::INTERRUPT_RATE_TICK); f++) {
if (fault_bits.test(f)) {
faults.set(i, cereal::PandaState::FaultType(f));
i++;
}
}
pm->send("pandaState", msg);
return ignition;
}
void send_peripheral_state(PubMaster *pm, Panda *panda) {
health_t pandaState = panda->get_state();
// build msg
MessageBuilder msg;
auto evt = msg.initEvent();
evt.setValid(panda->comms_healthy);
auto ps = evt.initPeripheralState();
ps.setPandaType(panda->hw_type);
if (Hardware::TICI()) {
double read_time = millis_since_boot();
ps.setVoltage(std::atoi(util::read_file("/sys/class/hwmon/hwmon1/in1_input").c_str()));
ps.setCurrent(std::atoi(util::read_file("/sys/class/hwmon/hwmon1/curr1_input").c_str()));
read_time = millis_since_boot() - read_time;
if (read_time > 50) {
LOGW("reading hwmon took %lfms", read_time);
}
} else {
ps.setVoltage(pandaState.voltage);
ps.setCurrent(pandaState.current);
}
uint16_t fan_speed_rpm = panda->get_fan_speed();
ps.setUsbPowerMode(cereal::PeripheralState::UsbPowerMode(pandaState.usb_power_mode));
ps.setFanSpeedRpm(fan_speed_rpm);
pm->send("peripheralState", msg);
}
void panda_state_thread(PubMaster *pm, Panda * peripheral_panda, Panda *panda, bool spoofing_started) {
Params params;
bool ignition_last = false;
Params params = Params();
LOGD("start panda state thread");
// run at 2hz
while (!do_exit && panda->connected) {
health_t pandaState = panda->get_state();
if (spoofing_started) {
pandaState.ignition_line = 1;
}
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
if (pandaState.safety_model == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT);
}
ignition = ((pandaState.ignition_line != 0) || (pandaState.ignition_can != 0));
if (ignition) {
no_ignition_cnt = 0;
} else {
no_ignition_cnt += 1;
}
#ifndef __x86_64__
bool power_save_desired = !ignition;
if (pandaState.power_save_enabled != power_save_desired) {
panda->set_power_saving(power_save_desired);
}
// set safety mode to NO_OUTPUT when car is off. ELM327 is an alternative if we want to leverage athenad/connect
if (!ignition && (pandaState.safety_model != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) {
panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT);
}
#endif
send_peripheral_state(pm, peripheral_panda);
ignition = send_panda_state(pm, panda, spoofing_started);
// clear VIN, CarParams, and set new safety on car start
if (ignition && !ignition_last) {
@ -294,87 +371,16 @@ void panda_state_thread(PubMaster *pm, Panda *panda, bool spoofing_started) {
params.clearAll(CLEAR_ON_IGNITION_OFF);
}
// Write to rtc once per minute when no ignition present
if ((panda->has_rtc) && !ignition && (no_ignition_cnt % 120 == 1)) {
// Write time to RTC if it looks reasonable
setenv("TZ","UTC",1);
struct tm sys_time = util::get_time();
if (util::time_valid(sys_time)) {
struct tm rtc_time = panda->get_rtc();
double seconds = difftime(mktime(&rtc_time), mktime(&sys_time));
if (std::abs(seconds) > 1.1) {
panda->set_rtc(sys_time);
LOGW("Updating panda RTC. dt = %.2f System: %s RTC: %s",
seconds, get_time_str(sys_time).c_str(), get_time_str(rtc_time).c_str());
}
}
}
ignition_last = ignition;
uint16_t fan_speed_rpm = panda->get_fan_speed();
// build msg
MessageBuilder msg;
auto evt = msg.initEvent();
evt.setValid(panda->comms_healthy);
auto ps = evt.initPandaState();
ps.setUptime(pandaState.uptime);
if (Hardware::TICI()) {
double read_time = millis_since_boot();
ps.setVoltage(std::atoi(util::read_file("/sys/class/hwmon/hwmon1/in1_input").c_str()));
ps.setCurrent(std::atoi(util::read_file("/sys/class/hwmon/hwmon1/curr1_input").c_str()));
read_time = millis_since_boot() - read_time;
if (read_time > 50) {
LOGW("reading hwmon took %lfms", read_time);
}
} else {
ps.setVoltage(pandaState.voltage);
ps.setCurrent(pandaState.current);
}
ps.setIgnitionLine(pandaState.ignition_line);
ps.setIgnitionCan(pandaState.ignition_can);
ps.setControlsAllowed(pandaState.controls_allowed);
ps.setGasInterceptorDetected(pandaState.gas_interceptor_detected);
ps.setHasGps(true);
ps.setCanRxErrs(pandaState.can_rx_errs);
ps.setCanSendErrs(pandaState.can_send_errs);
ps.setCanFwdErrs(pandaState.can_fwd_errs);
ps.setGmlanSendErrs(pandaState.gmlan_send_errs);
ps.setPandaType(panda->hw_type);
ps.setUsbPowerMode(cereal::PandaState::UsbPowerMode(pandaState.usb_power_mode));
ps.setSafetyModel(cereal::CarParams::SafetyModel(pandaState.safety_model));
ps.setSafetyParam(pandaState.safety_param);
ps.setFanSpeedRpm(fan_speed_rpm);
ps.setFaultStatus(cereal::PandaState::FaultStatus(pandaState.fault_status));
ps.setPowerSaveEnabled((bool)(pandaState.power_save_enabled));
ps.setHeartbeatLost((bool)(pandaState.heartbeat_lost));
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(pandaState.car_harness_status));
// Convert faults bitset to capnp list
std::bitset<sizeof(pandaState.faults) * 8> fault_bits(pandaState.faults);
auto faults = ps.initFaults(fault_bits.count());
size_t i = 0;
for (size_t f = size_t(cereal::PandaState::FaultType::RELAY_MALFUNCTION);
f <= size_t(cereal::PandaState::FaultType::INTERRUPT_RATE_TICK); f++) {
if (fault_bits.test(f)) {
faults.set(i, cereal::PandaState::FaultType(f));
i++;
}
}
pm->send("pandaState", msg);
panda->send_heartbeat();
util::sleep_for(500);
}
}
void hardware_control_thread(Panda *panda) {
LOGD("start hardware control thread");
void peripheral_control_thread(Panda *panda) {
LOGD("start peripheral control thread");
SubMaster sm({"deviceState", "driverCameraState"});
uint64_t last_front_frame_t = 0;
@ -395,10 +401,10 @@ void hardware_control_thread(Panda *panda) {
bool charging_disabled = sm["deviceState"].getDeviceState().getChargingDisabled();
if (charging_disabled != prev_charging_disabled) {
if (charging_disabled) {
panda->set_usb_power_mode(cereal::PandaState::UsbPowerMode::CLIENT);
panda->set_usb_power_mode(cereal::PeripheralState::UsbPowerMode::CLIENT);
LOGW("TURN OFF CHARGING!\n");
} else {
panda->set_usb_power_mode(cereal::PandaState::UsbPowerMode::CDP);
panda->set_usb_power_mode(cereal::PeripheralState::UsbPowerMode::CDP);
LOGW("TURN ON CHARGING!\n");
}
prev_charging_disabled = charging_disabled;
@ -444,6 +450,23 @@ void hardware_control_thread(Panda *panda) {
prev_ir_pwr = ir_pwr;
}
// Write to rtc once per minute when no ignition present
if ((panda->has_rtc) && !ignition && (cnt % 120 == 1)) {
// Write time to RTC if it looks reasonable
setenv("TZ","UTC",1);
struct tm sys_time = util::get_time();
if (util::time_valid(sys_time)) {
struct tm rtc_time = panda->get_rtc();
double seconds = difftime(mktime(&rtc_time), mktime(&sys_time));
if (std::abs(seconds) > 1.1) {
panda->set_rtc(sys_time);
LOGW("Updating panda RTC. dt = %.2f System: %s RTC: %s",
seconds, get_time_str(sys_time).c_str(), get_time_str(rtc_time).c_str());
}
}
}
}
}
@ -538,14 +561,16 @@ int main() {
LOG("set affinity returns %d", err);
LOGW("attempting to connect");
PubMaster pm({"pandaState"});
PubMaster pm({"pandaState", "peripheralState"});
while (!do_exit) {
Panda *panda = usb_connect();
Panda *peripheral_panda = panda;
// Send empty pandaState and try again
if (panda == nullptr) {
// Send empty pandaState & peripheralState and try again
if (panda == nullptr || peripheral_panda == nullptr) {
send_empty_panda_state(&pm);
send_empty_peripheral_state(&pm);
util::sleep_for(500);
continue;
}
@ -553,11 +578,12 @@ int main() {
LOGW("connected to board");
std::vector<std::thread> threads;
threads.emplace_back(panda_state_thread, &pm, panda, getenv("STARTED") != nullptr);
threads.emplace_back(panda_state_thread, &pm, peripheral_panda, panda, getenv("STARTED") != nullptr);
threads.emplace_back(peripheral_control_thread, peripheral_panda);
threads.emplace_back(pigeon_thread, peripheral_panda);
threads.emplace_back(can_send_thread, panda, getenv("FAKESEND") != nullptr);
threads.emplace_back(can_recv_thread, panda);
threads.emplace_back(hardware_control_thread, panda);
threads.emplace_back(pigeon_thread, panda);
for (auto &t : threads) t.join();

View File

@ -333,7 +333,7 @@ void Panda::set_power_saving(bool power_saving) {
usb_write(0xe7, power_saving, 0);
}
void Panda::set_usb_power_mode(cereal::PandaState::UsbPowerMode power_mode) {
void Panda::set_usb_power_mode(cereal::PeripheralState::UsbPowerMode power_mode) {
usb_write(0xe6, (uint16_t)power_mode, 0);
}

View File

@ -81,7 +81,7 @@ class Panda {
std::optional<std::vector<uint8_t>> get_firmware_version();
std::optional<std::string> get_serial();
void set_power_saving(bool power_saving);
void set_usb_power_mode(cereal::PandaState::UsbPowerMode power_mode);
void set_usb_power_mode(cereal::PeripheralState::UsbPowerMode power_mode);
void send_heartbeat();
void can_send(capnp::List<cereal::CanData>::Reader can_data_list);
int can_receive(kj::Array<capnp::word>& out_buf);

View File

@ -72,7 +72,7 @@ class Controls:
self.sm = sm
if self.sm is None:
ignore = ['driverCameraState', 'managerState'] if SIMULATION else None
self.sm = messaging.SubMaster(['deviceState', 'pandaState', 'modelV2', 'liveCalibration',
self.sm = messaging.SubMaster(['deviceState', 'pandaState', 'peripheralState', 'modelV2', 'liveCalibration',
'driverMonitoringState', 'longitudinalPlan', 'lateralPlan', 'liveLocationKalman',
'managerState', 'liveParameters', 'radarState'] + self.camera_packets + joystick_packet,
ignore_alive=ignore, ignore_avg_freq=['radarState', 'longitudinalPlan'])
@ -207,8 +207,8 @@ class Controls:
self.events.add(EventName.highCpuUsage)
# Alert if fan isn't spinning for 5 seconds
if self.sm['pandaState'].pandaType in [PandaType.uno, PandaType.dos]:
if self.sm['pandaState'].fanSpeedRpm == 0 and self.sm['deviceState'].fanSpeedPercentDesired > 50:
if self.sm['peripheralState'].pandaType in [PandaType.uno, PandaType.dos]:
if self.sm['peripheralState'].fanSpeedRpm == 0 and self.sm['deviceState'].fanSpeedPercentDesired > 50:
if (self.sm.frame - self.last_functional_fan_frame) * DT_CTRL > 5.0:
self.events.add(EventName.fanMalfunction)
else:

View File

@ -37,10 +37,6 @@
"text": "An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install.",
"severity": 0
},
"Offroad_HardwareUnsupported": {
"text": "White and grey panda are unsupported. Upgrade to comma two or black panda.",
"severity": 0
},
"Offroad_UnofficialHardware": {
"text": "Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, contact support@comma.ai.",
"severity": 1

View File

@ -204,7 +204,7 @@ def calibration_incomplete_alert(CP: car.CarParams, sm: messaging.SubMaster, met
def no_gps_alert(CP: car.CarParams, sm: messaging.SubMaster, metric: bool) -> Alert:
gps_integrated = sm['pandaState'].pandaType in [log.PandaState.PandaType.uno, log.PandaState.PandaType.dos]
gps_integrated = sm['peripheralState'].pandaType in [log.PandaState.PandaType.uno, log.PandaState.PandaType.dos]
return Alert(
"Poor GPS reception",
"If sky is visible, contact support" if gps_integrated else "Check GPS antenna placement",

View File

@ -240,7 +240,7 @@ CONFIGS = [
proc_name="controlsd",
pub_sub={
"can": ["controlsState", "carState", "carControl", "sendcan", "carEvents", "carParams"],
"deviceState": [], "pandaState": [], "liveCalibration": [], "driverMonitoringState": [], "longitudinalPlan": [], "lateralPlan": [], "liveLocationKalman": [], "liveParameters": [], "radarState": [],
"deviceState": [], "pandaState": [], "peripheralState": [], "liveCalibration": [], "driverMonitoringState": [], "longitudinalPlan": [], "lateralPlan": [], "liveLocationKalman": [], "liveParameters": [], "radarState": [],
"modelV2": [], "driverCameraState": [], "roadCameraState": [], "ubloxRaw": [], "managerState": [],
},
ignore=["logMonoTime", "valid", "controlsState.startMonoTime", "controlsState.cumLagMs"],

View File

@ -66,6 +66,7 @@ TIMINGS = {
# rtols: max/min, rsd
"can": [2.5, 0.35],
"pandaState": [2.5, 0.35],
"peripheralState": [2.5, 0.35],
"sendcan": [2.5, 0.35],
"carState": [2.5, 0.35],
"carControl": [2.5, 0.35],

View File

@ -28,8 +28,8 @@ class PowerMonitoring:
self.last_save_time = 0 # Used for saving current value in a param
self.power_used_uWh = 0 # Integrated power usage in uWh since going into offroad
self.next_pulsed_measurement_time = None
self.car_voltage_mV = 12e3 # Low-passed version of pandaState voltage
self.car_voltage_instant_mV = 12e3 # Last value of pandaState voltage
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()
car_battery_capacity_uWh = self.params.get("CarBatteryCapacity")
@ -39,14 +39,13 @@ class PowerMonitoring:
# Reset capacity if it's low
self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh))
# Calculation tick
def calculate(self, pandaState):
def calculate(self, peripheralState, ignition):
try:
now = sec_since_boot()
# If pandaState is None, we're probably not in a car, so we don't care
if pandaState is None or pandaState.pandaState.pandaType == log.PandaState.PandaType.unknown:
# If peripheralState is None, we're probably not in a car, so we don't care
if peripheralState is None or peripheralState.pandaType == log.PandaState.PandaType.unknown:
with self.integration_lock:
self.last_measurement_time = None
self.next_pulsed_measurement_time = None
@ -54,8 +53,8 @@ class PowerMonitoring:
return
# Low-pass battery voltage
self.car_voltage_instant_mV = pandaState.pandaState.voltage
self.car_voltage_mV = ((pandaState.pandaState.voltage * CAR_VOLTAGE_LOW_PASS_K) + (self.car_voltage_mV * (1 - CAR_VOLTAGE_LOW_PASS_K)))
self.car_voltage_instant_mV = peripheralState.voltage
self.car_voltage_mV = ((peripheralState.voltage * CAR_VOLTAGE_LOW_PASS_K) + (self.car_voltage_mV * (1 - CAR_VOLTAGE_LOW_PASS_K)))
# Cap the car battery power and save it in a param every 10-ish seconds
self.car_battery_capacity_uWh = max(self.car_battery_capacity_uWh, 0)
@ -70,7 +69,7 @@ class PowerMonitoring:
self.last_measurement_time = now
return
if (pandaState.pandaState.ignitionLine or pandaState.pandaState.ignitionCan):
if ignition:
# If there is ignition, we integrate the charging rate of the car
with self.integration_lock:
self.power_used_uWh = 0
@ -81,7 +80,7 @@ class PowerMonitoring:
self.last_measurement_time = now
else:
# No ignition, we integrate the offroad power used by the device
is_uno = pandaState.pandaState.pandaType == log.PandaState.PandaType.uno
is_uno = peripheralState.pandaType == log.PandaState.PandaType.uno
# Get current power draw somehow
current_power = HARDWARE.get_current_power_draw() # pylint: disable=assignment-from-none
if current_power is not None:
@ -158,8 +157,8 @@ class PowerMonitoring:
return int(self.car_battery_capacity_uWh)
# See if we need to disable charging
def should_disable_charging(self, pandaState, offroad_timestamp):
if pandaState is None or offroad_timestamp is None:
def should_disable_charging(self, ignition, in_car, offroad_timestamp):
if offroad_timestamp is None:
return False
now = sec_since_boot()
@ -167,24 +166,24 @@ class PowerMonitoring:
disable_charging |= (now - offroad_timestamp) > MAX_TIME_OFFROAD_S
disable_charging |= (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3)) and (self.car_voltage_instant_mV > (VBATT_INSTANT_PAUSE_CHARGING * 1e3))
disable_charging |= (self.car_battery_capacity_uWh <= 0)
disable_charging &= (not pandaState.pandaState.ignitionLine and not pandaState.pandaState.ignitionCan)
disable_charging &= not ignition
disable_charging &= (not self.params.get_bool("DisablePowerDown"))
disable_charging &= (pandaState.pandaState.harnessStatus != log.PandaState.HarnessStatus.notConnected)
disable_charging &= in_car
disable_charging |= self.params.get_bool("ForcePowerDown")
return disable_charging
# See if we need to shutdown
def should_shutdown(self, pandaState, offroad_timestamp, started_seen):
if pandaState is None or offroad_timestamp is None:
def should_shutdown(self, peripheralState, ignition, in_car, offroad_timestamp, started_seen):
if offroad_timestamp is None:
return False
now = sec_since_boot()
panda_charging = (pandaState.pandaState.usbPowerMode != log.PandaState.UsbPowerMode.client)
panda_charging = (peripheralState.usbPowerMode != log.PeripheralState.UsbPowerMode.client)
BATT_PERC_OFF = 10
should_shutdown = False
# Wait until we have shut down charging before powering down
should_shutdown |= (not panda_charging and self.should_disable_charging(pandaState, offroad_timestamp))
should_shutdown |= (not panda_charging and self.should_disable_charging(ignition, in_car, offroad_timestamp))
should_shutdown |= ((HARDWARE.get_battery_capacity() < BATT_PERC_OFF) and (not HARDWARE.get_battery_charging()) and ((now - offroad_timestamp) > 60))
should_shutdown &= started_seen or (now > MIN_ON_TIME_S)
return should_shutdown

View File

@ -37,20 +37,17 @@ class TestPowerMonitoring(unittest.TestCase):
params.delete("CarBatteryCapacity")
params.delete("DisablePowerDown")
def mock_pandaState(self, ignition, hw_type, car_voltage=12, harness_status=log.PandaState.HarnessStatus.normal):
pandaState = messaging.new_message('pandaState')
pandaState.pandaState.pandaType = hw_type
pandaState.pandaState.voltage = car_voltage * 1e3
pandaState.pandaState.ignitionLine = ignition
pandaState.pandaState.ignitionCan = False
pandaState.pandaState.harnessStatus = harness_status
return pandaState
def mock_peripheralState(self, hw_type, car_voltage=12):
ps = messaging.new_message('peripheralState').peripheralState
ps.pandaType = hw_type
ps.voltage = car_voltage * 1e3
return ps
# Test to see that it doesn't do anything when pandaState is None
def test_pandaState_present(self):
pm = PowerMonitoring()
for _ in range(10):
pm.calculate(None)
pm.calculate(None, None)
self.assertEqual(pm.get_power_used(), 0)
self.assertEqual(pm.get_car_battery_capacity(), (CAR_BATTERY_CAPACITY_uWh / 10))
@ -59,7 +56,7 @@ class TestPowerMonitoring(unittest.TestCase):
def test_offroad_ignition(self, hw_type):
pm = PowerMonitoring()
for _ in range(10):
pm.calculate(self.mock_pandaState(True, hw_type))
pm.calculate(self.mock_peripheralState(hw_type), True)
self.assertEqual(pm.get_power_used(), 0)
# Test to see that it integrates with discharging battery
@ -71,7 +68,7 @@ class TestPowerMonitoring(unittest.TestCase):
pm_patch("HARDWARE.get_battery_status", "Discharging"), pm_patch("HARDWARE.get_current_power_draw", None):
pm = PowerMonitoring()
for _ in range(TEST_DURATION_S + 1):
pm.calculate(self.mock_pandaState(False, hw_type))
pm.calculate(self.mock_peripheralState(hw_type), False)
expected_power_usage = ((TEST_DURATION_S/3600) * (BATT_VOLTAGE * BATT_CURRENT) * 1e6)
self.assertLess(abs(pm.get_power_used() - expected_power_usage), 10)
@ -85,7 +82,7 @@ class TestPowerMonitoring(unittest.TestCase):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = 0
for _ in range(TEST_DURATION_S + 1):
pm.calculate(self.mock_pandaState(True, hw_type))
pm.calculate(self.mock_peripheralState(hw_type), True)
expected_capacity = ((TEST_DURATION_S/3600) * CAR_CHARGING_RATE_W * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
@ -99,7 +96,7 @@ class TestPowerMonitoring(unittest.TestCase):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh - 1000
for _ in range(TEST_DURATION_S + 1):
pm.calculate(self.mock_pandaState(True, hw_type))
pm.calculate(self.mock_peripheralState(hw_type), True)
estimated_capacity = CAR_BATTERY_CAPACITY_uWh + (CAR_CHARGING_RATE_W / 3600 * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - estimated_capacity), 10)
@ -113,7 +110,7 @@ class TestPowerMonitoring(unittest.TestCase):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
for _ in range(TEST_DURATION_S + 1):
pm.calculate(self.mock_pandaState(False, hw_type))
pm.calculate(self.mock_peripheralState(hw_type), False)
expected_capacity = CAR_BATTERY_CAPACITY_uWh - ((TEST_DURATION_S/3600) * (BATT_VOLTAGE * BATT_CURRENT) * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
@ -127,7 +124,7 @@ class TestPowerMonitoring(unittest.TestCase):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = 1000
for _ in range(TEST_DURATION_S + 1):
pm.calculate(self.mock_pandaState(False, hw_type))
pm.calculate(self.mock_peripheralState(hw_type), False)
estimated_capacity = 0 - ((1/3600) * (BATT_VOLTAGE * BATT_CURRENT) * 1e6)
self.assertLess(abs(pm.get_car_battery_capacity() - estimated_capacity), 10)
@ -143,12 +140,13 @@ class TestPowerMonitoring(unittest.TestCase):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
start_time = ssb
pandaState = self.mock_pandaState(False, hw_type)
ignition = False
peripheralState = self.mock_peripheralState(hw_type)
while ssb <= start_time + MOCKED_MAX_OFFROAD_TIME:
pm.calculate(pandaState)
pm.calculate(peripheralState, ignition)
if (ssb - start_time) % 1000 == 0 and ssb < start_time + MOCKED_MAX_OFFROAD_TIME:
self.assertFalse(pm.should_disable_charging(pandaState, start_time))
self.assertTrue(pm.should_disable_charging(pandaState, start_time))
self.assertFalse(pm.should_disable_charging(ignition, True, start_time))
self.assertTrue(pm.should_disable_charging(ignition, True, start_time))
# Test to check policy of stopping charging when the car voltage is too low
@parameterized.expand(ALL_PANDA_TYPES)
@ -160,12 +158,13 @@ class TestPowerMonitoring(unittest.TestCase):
pm_patch("HARDWARE.get_battery_status", "Discharging"), pm_patch("HARDWARE.get_current_power_draw", None):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
pandaState = self.mock_pandaState(False, hw_type, car_voltage=(VBATT_PAUSE_CHARGING - 1))
ignition = False
peripheralState = self.mock_peripheralState(hw_type, car_voltage=(VBATT_PAUSE_CHARGING - 1))
for i in range(TEST_TIME):
pm.calculate(pandaState)
pm.calculate(peripheralState, ignition)
if i % 10 == 0:
self.assertEqual(pm.should_disable_charging(pandaState, ssb), (pm.car_voltage_mV < VBATT_PAUSE_CHARGING*1e3))
self.assertTrue(pm.should_disable_charging(pandaState, ssb))
self.assertEqual(pm.should_disable_charging(ignition, True, ssb), (pm.car_voltage_mV < VBATT_PAUSE_CHARGING*1e3))
self.assertTrue(pm.should_disable_charging(ignition, True, ssb))
# Test to check policy of not stopping charging when DisablePowerDown is set
def test_disable_power_down(self):
@ -177,12 +176,13 @@ class TestPowerMonitoring(unittest.TestCase):
pm_patch("HARDWARE.get_battery_status", "Discharging"), pm_patch("HARDWARE.get_current_power_draw", None):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
pandaState = self.mock_pandaState(False, log.PandaState.PandaType.uno, car_voltage=(VBATT_PAUSE_CHARGING - 1))
ignition = False
peripheralState = self.mock_peripheralState(log.PandaState.PandaType.uno, car_voltage=(VBATT_PAUSE_CHARGING - 1))
for i in range(TEST_TIME):
pm.calculate(pandaState)
pm.calculate(peripheralState, ignition)
if i % 10 == 0:
self.assertFalse(pm.should_disable_charging(pandaState, ssb))
self.assertFalse(pm.should_disable_charging(pandaState, ssb))
self.assertFalse(pm.should_disable_charging(ignition, True, ssb))
self.assertFalse(pm.should_disable_charging(ignition, True, ssb))
# Test to check policy of not stopping charging when ignition
def test_ignition(self):
@ -193,12 +193,13 @@ class TestPowerMonitoring(unittest.TestCase):
pm_patch("HARDWARE.get_battery_status", "Discharging"), pm_patch("HARDWARE.get_current_power_draw", None):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
pandaState = self.mock_pandaState(True, log.PandaState.PandaType.uno, car_voltage=(VBATT_PAUSE_CHARGING - 1))
ignition = True
peripheralState = self.mock_peripheralState(log.PandaState.PandaType.uno, car_voltage=(VBATT_PAUSE_CHARGING - 1))
for i in range(TEST_TIME):
pm.calculate(pandaState)
pm.calculate(peripheralState, ignition)
if i % 10 == 0:
self.assertFalse(pm.should_disable_charging(pandaState, ssb))
self.assertFalse(pm.should_disable_charging(pandaState, ssb))
self.assertFalse(pm.should_disable_charging(ignition, True, ssb))
self.assertFalse(pm.should_disable_charging(ignition, True, ssb))
# Test to check policy of not stopping charging when harness is not connected
def test_harness_connection(self):
@ -209,13 +210,14 @@ class TestPowerMonitoring(unittest.TestCase):
pm_patch("HARDWARE.get_battery_status", "Discharging"), pm_patch("HARDWARE.get_current_power_draw", None):
pm = PowerMonitoring()
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
pandaState = self.mock_pandaState(False, log.PandaState.PandaType.uno, car_voltage=(VBATT_PAUSE_CHARGING - 1),
harness_status=log.PandaState.HarnessStatus.notConnected)
ignition = False
peripheralState = self.mock_peripheralState(log.PandaState.PandaType.uno, car_voltage=(VBATT_PAUSE_CHARGING - 1))
for i in range(TEST_TIME):
pm.calculate(pandaState)
pm.calculate(peripheralState, ignition)
if i % 10 == 0:
self.assertFalse(pm.should_disable_charging(pandaState, ssb))
self.assertFalse(pm.should_disable_charging(pandaState, ssb))
self.assertFalse(pm.should_disable_charging(ignition, False,ssb))
self.assertFalse(pm.should_disable_charging(ignition, False, ssb))
if __name__ == "__main__":
unittest.main()

View File

@ -155,8 +155,7 @@ def thermald_thread():
pandaState_timeout = int(1000 * 2.5 * DT_TRML) # 2.5x the expected pandaState frequency
pandaState_sock = messaging.sub_sock('pandaState', timeout=pandaState_timeout)
location_sock = messaging.sub_sock('gpsLocationExternal')
managerState_sock = messaging.sub_sock('managerState', conflate=True)
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "managerState"])
fan_speed = 0
count = 0
@ -184,6 +183,7 @@ def thermald_thread():
temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML)
pandaState_prev = None
should_start_prev = False
in_car = False
handle_fan = None
is_uno = False
ui_running_prev = False
@ -202,15 +202,19 @@ def thermald_thread():
if params.get_bool("IsOnroad"):
params.put_bool("BootedOnroad", True)
while 1:
while True:
pandaState = messaging.recv_sock(pandaState_sock, wait=True)
sm.update(0)
peripheralState = sm['peripheralState']
msg = read_thermal(thermal_config)
if pandaState is not None:
usb_power = pandaState.pandaState.usbPowerMode != log.PandaState.UsbPowerMode.client
pandaState = pandaState.pandaState
# If we lose connection to the panda, wait 5 seconds before going offroad
if pandaState.pandaState.pandaType == log.PandaState.PandaType.unknown:
if pandaState.pandaType == log.PandaState.PandaType.unknown:
no_panda_cnt += 1
if no_panda_cnt > DISCONNECT_TIMEOUT / DT_TRML:
if startup_conditions["ignition"]:
@ -218,15 +222,14 @@ def thermald_thread():
startup_conditions["ignition"] = False
else:
no_panda_cnt = 0
startup_conditions["ignition"] = pandaState.pandaState.ignitionLine or pandaState.pandaState.ignitionCan
startup_conditions["ignition"] = pandaState.ignitionLine or pandaState.ignitionCan
startup_conditions["hardware_supported"] = pandaState.pandaState.pandaType not in [log.PandaState.PandaType.whitePanda,
log.PandaState.PandaType.greyPanda]
set_offroad_alert_if_changed("Offroad_HardwareUnsupported", not startup_conditions["hardware_supported"])
in_car = pandaState.harnessStatus != log.PandaState.HarnessStatus.notConnected
usb_power = peripheralState.usbPowerMode != log.PeripheralState.UsbPowerMode.client
# Setup fan handler on first connect to panda
if handle_fan is None and pandaState.pandaState.pandaType != log.PandaState.PandaType.unknown:
is_uno = pandaState.pandaState.pandaType == log.PandaState.PandaType.uno
if handle_fan is None and peripheralState.pandaType != log.PandaState.PandaType.unknown:
is_uno = peripheralState.pandaType == log.PandaState.PandaType.uno
if TICI:
cloudlog.info("Setting up TICI fan handler")
@ -241,8 +244,8 @@ def thermald_thread():
# Handle disconnect
if pandaState_prev is not None:
if pandaState.pandaState.pandaType == log.PandaState.PandaType.unknown and \
pandaState_prev.pandaState.pandaType != log.PandaState.PandaType.unknown:
if pandaState.pandaType == log.PandaState.PandaType.unknown and \
pandaState_prev.pandaType != log.PandaState.PandaType.unknown:
params.clear_all(ParamKeyType.CLEAR_ON_PANDA_DISCONNECT)
pandaState_prev = pandaState
@ -399,27 +402,25 @@ def thermald_thread():
off_ts = sec_since_boot()
# Offroad power monitoring
power_monitor.calculate(pandaState)
power_monitor.calculate(peripheralState, startup_conditions["ignition"])
msg.deviceState.offroadPowerUsageUwh = power_monitor.get_power_used()
msg.deviceState.carBatteryCapacityUwh = max(0, power_monitor.get_car_battery_capacity())
# Check if we need to disable charging (handled by boardd)
msg.deviceState.chargingDisabled = power_monitor.should_disable_charging(pandaState, off_ts)
msg.deviceState.chargingDisabled = power_monitor.should_disable_charging(startup_conditions["ignition"], in_car, off_ts)
# Check if we need to shut down
if power_monitor.should_shutdown(pandaState, off_ts, started_seen):
if power_monitor.should_shutdown(peripheralState, startup_conditions["ignition"], in_car, off_ts, started_seen):
cloudlog.info(f"shutting device down, offroad since {off_ts}")
# TODO: add function for blocking cloudlog instead of sleep
time.sleep(10)
HARDWARE.shutdown()
# If UI has crashed, set the brightness to reasonable non-zero value
manager_state = messaging.recv_one_or_none(managerState_sock)
if manager_state is not None:
ui_running = "ui" in (p.name for p in manager_state.managerState.processes if p.running)
if ui_running_prev and not ui_running:
HARDWARE.set_screen_brightness(20)
ui_running_prev = ui_running
ui_running = "ui" in (p.name for p in sm["managerState"].processes if p.running)
if ui_running_prev and not ui_running:
HARDWARE.set_screen_brightness(20)
ui_running_prev = ui_running
msg.deviceState.chargingError = current_filter.x > 0. and msg.deviceState.batteryPercent < 90 # if current is positive, then battery is being discharged
msg.deviceState.started = started_ts is not None
@ -443,11 +444,11 @@ def thermald_thread():
if EON and started_ts is None and msg.deviceState.memoryUsagePercent > 40:
cloudlog.event("High offroad memory usage", mem=msg.deviceState.memoryUsagePercent)
location = messaging.recv_sock(location_sock)
cloudlog.event("STATUS_PACKET",
count=count,
pandaState=(strip_deprecated_keys(pandaState.to_dict()) if pandaState else None),
location=(strip_deprecated_keys(location.gpsLocationExternal.to_dict()) if location else None),
peripheralState=strip_deprecated_keys(peripheralState.to_dict()),
location=(strip_deprecated_keys(sm["gpsLocationExternal"].to_dict()) if sm.alive["gpsLocationExternal"] else None),
deviceState=strip_deprecated_keys(msg.to_dict()))
count += 1