diff --git a/frogpilot/common/frogpilot_functions.py b/frogpilot/common/frogpilot_functions.py
index b5bf3a5c..40b2d35f 100644
--- a/frogpilot/common/frogpilot_functions.py
+++ b/frogpilot/common/frogpilot_functions.py
@@ -15,7 +15,7 @@ from openpilot.frogpilot.assets.theme_manager import ThemeManager
from openpilot.frogpilot.common.frogpilot_backups import backup_frogpilot
from openpilot.frogpilot.common.frogpilot_utilities import is_FrogsGoMoo, run_cmd
from openpilot.frogpilot.common.frogpilot_variables import (
- FROGS_GO_MOO_PATH, THEME_SAVE_PATH,
+ ERROR_LOGS_PATH, FROGS_GO_MOO_PATH, THEME_SAVE_PATH,
FrogPilotVariables, get_frogpilot_toggles
)
@@ -38,6 +38,7 @@ def frogpilot_boot_functions(build_metadata, params):
def install_frogpilot(build_metadata, params):
paths = [
+ ERROR_LOGS_PATH,
THEME_SAVE_PATH
]
for path in paths:
diff --git a/frogpilot/controls/frogpilot_card.py b/frogpilot/controls/frogpilot_card.py
index 2942f59c..26836484 100644
--- a/frogpilot/controls/frogpilot_card.py
+++ b/frogpilot/controls/frogpilot_card.py
@@ -5,7 +5,7 @@ from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS, ButtonType
from openpilot.selfdrive.selfdrived.events import ET
from openpilot.frogpilot.common.frogpilot_utilities import is_FrogsGoMoo
-from openpilot.frogpilot.common.frogpilot_variables import NON_DRIVING_GEARS
+from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, NON_DRIVING_GEARS
from openpilot.frogpilot.controls.lib.conditional_experimental_mode import CEStatus
class FrogPilotCard:
@@ -28,6 +28,8 @@ class FrogPilotCard:
self.long_press_threshold = CRUISE_LONG_PRESS * (1.5 if self.CP.brand == "gm" else 1)
self.very_long_press_threshold = CRUISE_LONG_PRESS * 5
+ self.error_log = ERROR_LOGS_PATH / "error.txt"
+
def handle_button_event(self, key, sm, frogpilot_toggles):
if sm["carControl"].longActive and getattr(frogpilot_toggles, f"experimental_mode_via_{key}"):
self.handle_experimental_mode(sm, frogpilot_toggles)
@@ -61,6 +63,7 @@ class FrogPilotCard:
self.always_on_lateral_enabled &= sm["liveCalibration"].calPerc >= 1
self.always_on_lateral_enabled &= (ET.IMMEDIATE_DISABLE not in sm["selfdriveState"].alertType + sm["frogpilotSelfdriveState"].alertType) or self.frogs_go_moo
self.always_on_lateral_enabled &= not (carState.brakePressed and carState.vEgo < frogpilot_toggles.always_on_lateral_pause_speed) or carState.standstill
+ self.always_on_lateral_enabled &= not self.error_log.is_file() or self.frogs_go_moo
if sm.updated["frogpilotPlan"] or any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in carState.buttonEvents):
self.accel_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in carState.buttonEvents)
diff --git a/frogpilot/controls/frogpilot_planner.py b/frogpilot/controls/frogpilot_planner.py
index 350e63a0..d14c6f53 100644
--- a/frogpilot/controls/frogpilot_planner.py
+++ b/frogpilot/controls/frogpilot_planner.py
@@ -20,13 +20,13 @@ from openpilot.frogpilot.controls.lib.frogpilot_following import FrogPilotFollow
from openpilot.frogpilot.controls.lib.frogpilot_vcruise import FrogPilotVCruise
class FrogPilotPlanner:
- def __init__(self):
+ def __init__(self, error_log):
self.params = Params(return_defaults=True)
self.params_memory = Params(memory=True)
self.frogpilot_acceleration = FrogPilotAcceleration(self)
self.frogpilot_cem = ConditionalExperimentalMode(self)
- self.frogpilot_events = FrogPilotEvents(self)
+ self.frogpilot_events = FrogPilotEvents(self, error_log)
self.frogpilot_following = FrogPilotFollowing(self)
self.frogpilot_vcruise = FrogPilotVCruise(self)
diff --git a/frogpilot/controls/lib/frogpilot_events.py b/frogpilot/controls/lib/frogpilot_events.py
index 89117c52..75717330 100644
--- a/frogpilot/controls/lib/frogpilot_events.py
+++ b/frogpilot/controls/lib/frogpilot_events.py
@@ -2,7 +2,7 @@
from openpilot.selfdrive.selfdrived.events import ET, EVENT_NAME, FROGPILOT_EVENT_NAME, EventName, FrogPilotEventName, Events
class FrogPilotEvents:
- def __init__(self, FrogPilotPlanner):
+ def __init__(self, FrogPilotPlanner, error_log):
self.frogpilot_planner = FrogPilotPlanner
self.events = Events(frogpilot=True)
@@ -13,6 +13,8 @@ class FrogPilotEvents:
self.played_events = set()
+ self.error_log = error_log
+
def update(self, v_cruise, sm, frogpilot_toggles):
current_alert = sm["selfdriveState"].alertType
current_frogpilot_alert = sm["selfdriveState"].alertType
@@ -28,6 +30,9 @@ class FrogPilotEvents:
else:
self.max_acceleration = 0
+ if self.error_log.is_file():
+ self.events.add(FrogPilotEventName.openpilotCrashed)
+
self.startup_seen |= sm["frogpilotSelfdriveState"].alertText1 == frogpilot_toggles.startup_alert_top and sm["frogpilotSelfdriveState"].alertText2 == frogpilot_toggles.startup_alert_bottom
self.played_events.update(FROGPILOT_EVENT_NAME[event] for event in self.events.names)
diff --git a/frogpilot/frogpilot_process.py b/frogpilot/frogpilot_process.py
index eeb2b5c3..108b0dff 100644
--- a/frogpilot/frogpilot_process.py
+++ b/frogpilot/frogpilot_process.py
@@ -12,7 +12,7 @@ from openpilot.frogpilot.assets.theme_manager import THEME_COMPONENT_PARAMS, The
from openpilot.frogpilot.common.frogpilot_backups import backup_toggles
from openpilot.frogpilot.common.frogpilot_functions import update_openpilot
from openpilot.frogpilot.common.frogpilot_utilities import ThreadManager, is_url_pingable
-from openpilot.frogpilot.common.frogpilot_variables import FrogPilotVariables
+from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, FrogPilotVariables
from openpilot.frogpilot.controls.frogpilot_planner import FrogPilotPlanner
from openpilot.frogpilot.system.frogpilot_stats import send_stats
from openpilot.frogpilot.system.frogpilot_tracking import FrogPilotTracking
@@ -31,7 +31,9 @@ def transition_offroad(frogpilot_planner, thread_manager, time_validated, sm, pa
if time_validated:
thread_manager.run_with_lock(send_stats, (params, frogpilot_toggles))
-def transition_onroad():
+def transition_onroad(error_log):
+ if error_log.is_file():
+ error_log.unlink()
def update_checks(now, theme_manager, thread_manager, params, params_memory, frogpilot_toggles, boot_run=False):
while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")):
@@ -80,6 +82,10 @@ def frogpilot_thread():
started_previously = False
time_validated = False
+ error_log = ERROR_LOGS_PATH / "error.txt"
+ if error_log.is_file():
+ error_log.unlink()
+
while True:
sm.update()
@@ -93,10 +99,10 @@ def frogpilot_thread():
run_update_checks = True
elif started and not started_previously:
- frogpilot_planner = FrogPilotPlanner()
+ frogpilot_planner = FrogPilotPlanner(error_log)
frogpilot_tracking = FrogPilotTracking(frogpilot_planner, frogpilot_toggles)
- transition_onroad()
+ transition_onroad(error_log)
if started and sm.updated["modelV2"]:
frogpilot_planner.update(now, time_validated, sm, frogpilot_toggles)
diff --git a/frogpilot/ui/qt/offroad/data_settings.cc b/frogpilot/ui/qt/offroad/data_settings.cc
index 7c05acf0..2b710ec3 100644
--- a/frogpilot/ui/qt/offroad/data_settings.cc
+++ b/frogpilot/ui/qt/offroad/data_settings.cc
@@ -57,6 +57,36 @@ FrogPilotDataPanel::FrogPilotDataPanel(FrogPilotSettingsWindow *parent, bool for
}
dataMainList->addItem(deleteDrivingDataButton);
+ ButtonControl *deleteErrorLogsButton = new ButtonControl(tr("Delete Error Logs"), tr("DELETE"), tr("Delete collected error logs to free up space and clear old crash records."));
+ QObject::connect(deleteErrorLogsButton, &ButtonControl::clicked, [=]() {
+ QDir errorLogsDir("/data/error_logs");
+
+ if (ConfirmationDialog::confirm(tr("Delete all error logs?"), tr("Delete"), this)) {
+ std::thread([=]() mutable {
+ parent->keepScreenOn = true;
+
+ deleteErrorLogsButton->setEnabled(false);
+ deleteErrorLogsButton->setValue(tr("Deleting..."));
+
+ errorLogsDir.removeRecursively();
+ errorLogsDir.mkpath(".");
+
+ deleteErrorLogsButton->setValue(tr("Deleted!"));
+
+ util::sleep_for(2500);
+
+ deleteErrorLogsButton->setEnabled(true);
+ deleteErrorLogsButton->setValue("");
+
+ parent->keepScreenOn = false;
+ }).detach();
+ }
+ });
+ if (forceOpenDescriptions) {
+ deleteErrorLogsButton->showDescription();
+ }
+ dataMainList->addItem(deleteErrorLogsButton);
+
FrogPilotButtonsControl *frogpilotBackupButton = new FrogPilotButtonsControl(tr("FrogPilot Backups"), tr("Create, delete, or restore FrogPilot backups."), "", {tr("BACKUP"), tr("DELETE"), tr("DELETE ALL"), tr("RESTORE")});
QObject::connect(frogpilotBackupButton, &FrogPilotButtonsControl::buttonClicked, [=](int id) {
QDir backupDir("/data/backups");
diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py
index 9e80c576..2749d80c 100644
--- a/selfdrive/selfdrived/events.py
+++ b/selfdrive/selfdrived/events.py
@@ -1054,6 +1054,20 @@ FROGPILOT_EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
FrogPilotEventName.customStartupAlert: {
ET.PERMANENT: custom_startup_alert,
},
+
+ FrogPilotEventName.openpilotCrashed: {
+ ET.IMMEDIATE_DISABLE: Alert(
+ "openpilot crashed",
+ "Please post the 'Error Log' in the FrogPilot Discord!",
+ AlertStatus.critical, AlertSize.mid,
+ Priority.HIGHEST, VisualAlert.none, AudibleAlert.prompt, .1),
+
+ ET.NO_ENTRY: Alert(
+ "openpilot crashed",
+ "Please post the 'Error Log' in the FrogPilot Discord!",
+ AlertStatus.critical, AlertSize.mid,
+ Priority.HIGHEST, VisualAlert.none, AudibleAlert.prompt, .1),
+ },
}
diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc
index 0922c245..5f411787 100644
--- a/selfdrive/ui/qt/offroad/software_settings.cc
+++ b/selfdrive/ui/qt/offroad/software_settings.cc
@@ -110,6 +110,14 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) {
});
addItem(uninstallBtn);
+ // error log button
+ auto errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), tr("View the error log for openpilot crashes."));
+ connect(errorLogBtn, &ButtonControl::clicked, [=]() {
+ std::string txt = util::read_file("/data/error_logs/error.txt");
+ ConfirmationDialog::rich(QString::fromStdString(txt), this);
+ });
+ addItem(errorLogBtn);
+
fs_watch = new ParamWatcher(this);
QObject::connect(fs_watch, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) {
updateLabels();
diff --git a/selfdrive/ui/qt/onroad/alerts.cc b/selfdrive/ui/qt/onroad/alerts.cc
index b316fe00..17c3f83a 100644
--- a/selfdrive/ui/qt/onroad/alerts.cc
+++ b/selfdrive/ui/qt/onroad/alerts.cc
@@ -32,7 +32,15 @@ OnroadAlerts::Alert OnroadAlerts::getAlert(const SubMaster &sm, const SubMaster
const cereal::FrogPilotSelfdriveState::Reader &fpss = fpsm["frogpilotSelfdriveState"].getFrogpilotSelfdriveState();
Alert a = {};
- if (selfdrive_frame >= started_frame) { // Don't get old alert.
+ static QString crash_log_path = "/data/error_logs/error.txt";
+ if (QFile::exists(crash_log_path)) {
+ a = {tr("openpilot crashed"),
+ tr("Please post the \"Error Log\" in the FrogPilot Discord!"),
+ "openpilotCrashed",
+ cereal::SelfdriveState::AlertSize::MID,
+ cereal::SelfdriveState::AlertStatus::CRITICAL};
+ return a;
+ } else if (selfdrive_frame >= started_frame) { // Don't get old alert.
a = {ss.getAlertText1().cStr(), ss.getAlertText2().cStr(),
ss.getAlertType().cStr(), ss.getAlertSize(), ss.getAlertStatus()};
diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py
index 09fde839..50cced31 100644
--- a/selfdrive/ui/soundd.py
+++ b/selfdrive/ui/soundd.py
@@ -16,7 +16,7 @@ from openpilot.common.swaglog import cloudlog
from openpilot.system import micd
from openpilot.system.hardware import HARDWARE
-from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, get_frogpilot_toggles
+from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, get_frogpilot_toggles
SAMPLE_RATE = 48000
SAMPLE_BUFFER = 4096 # (approx 100ms)
@@ -84,10 +84,14 @@ class Soundd:
self.frogpilot_toggles = get_frogpilot_toggles()
+ self.openpilot_crashed_played = False
+
self.auto_volume = 0
self.previous_sound_pack = None
+ self.error_log = ERROR_LOGS_PATH / "error.txt"
+
self.update_frogpilot_sounds()
def load_sounds(self):
@@ -154,6 +158,9 @@ class Soundd:
if self.params_memory.get("TestAlert"):
self.update_alert(getattr(AudibleAlert, self.params_memory.get("TestAlert")))
self.params_memory.remove("TestAlert")
+ elif not self.openpilot_crashed_played and self.error_log.is_file():
+ self.update_alert(AudibleAlert.prompt)
+ self.openpilot_crashed_played = True
elif sm.updated['selfdriveState']:
new_alert = sm['selfdriveState'].alertSound.raw
diff --git a/system/sentry.py b/system/sentry.py
index 8303f37f..eac5bb44 100644
--- a/system/sentry.py
+++ b/system/sentry.py
@@ -2,6 +2,7 @@
import os
import sentry_sdk
import traceback
+from datetime import datetime
from enum import Enum
from sentry_sdk.integrations.threading import ThreadingIntegration
@@ -11,6 +12,8 @@ from openpilot.system.hardware import HARDWARE, PC
from openpilot.common.swaglog import cloudlog
from openpilot.system.version import get_build_metadata, get_version
+from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH
+
class SentryProject(Enum):
# python project
@@ -35,7 +38,7 @@ def capture_block() -> None:
sentry_sdk.flush()
-def capture_exception(*args, **kwargs) -> None:
+def capture_exception(*args, crash_log=True, **kwargs) -> None:
exc_text = traceback.format_exc()
errors_to_ignore = [
@@ -44,6 +47,7 @@ def capture_exception(*args, **kwargs) -> None:
if any(error in exc_text for error in errors_to_ignore):
return
+ save_exception(exc_text, crash_log)
cloudlog.error("crash", exc_info=kwargs.get('exc_info', 1))
try:
@@ -57,6 +61,20 @@ def set_tag(key: str, value: str) -> None:
sentry_sdk.set_tag(key, value)
+def save_exception(exc_text: str, crash_log) -> None:
+ files = [
+ ERROR_LOGS_PATH / datetime.now().astimezone().strftime("%Y-%m-%d--%H-%M-%S.log"),
+ ERROR_LOGS_PATH / "error.txt"
+ ]
+
+ for file_path in files:
+ if file_path.name == "error.txt" and crash_log:
+ lines = exc_text.splitlines()[-10:]
+ file_path.write_text("\n".join(lines))
+ else:
+ file_path.write_text(exc_text)
+
+
def init(project: SentryProject) -> bool:
build_metadata = get_build_metadata()
# forks like to mess with this, so double check