diff --git a/frogpilot/common/frogpilot_functions.py b/frogpilot/common/frogpilot_functions.py
index 65251f8b..f9b8c5da 100644
--- a/frogpilot/common/frogpilot_functions.py
+++ b/frogpilot/common/frogpilot_functions.py
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
+import io
+import json
import random
+import requests
import string
import threading
import time
@@ -16,11 +19,50 @@ 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, use_konik_server
from openpilot.frogpilot.common.frogpilot_variables import (
- ERROR_LOGS_PATH, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, MAPD_PATH, MAPS_PATH, THEME_SAVE_PATH,
+ DISCORD_WEBHOOK_URL_REPORT, ERROR_LOGS_PATH, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, MAPD_PATH, MAPS_PATH, THEME_SAVE_PATH,
FrogPilotVariables, get_frogpilot_toggles
)
+def capture_report(discord_user, report, frogpilot_toggles):
+ if not DISCORD_WEBHOOK_URL_REPORT:
+ return
+
+ error_file_path = ERROR_LOGS_PATH / "error.txt"
+ error_content = "No error log found."
+ if error_file_path.exists():
+ error_content = error_file_path.read_text()[:1000]
+
+ message = (
+ f"**🚨 New Error Report**\n\n"
+ f"**User:** `{discord_user}`\n\n"
+ f"**Report:**\n```{report}```\n\n"
+ f"**Error Log:**\n```{error_content}```\n\n"
+ f"**Toggle Settings:**"
+ )
+
+ try:
+ main_response = requests.post(
+ DISCORD_WEBHOOK_URL_REPORT,
+ data={"content": message},
+ files={"file": ("frogpilot_toggles.json", io.BytesIO(json.dumps(frogpilot_toggles, indent=2).encode("utf-8")), "application/json")},
+ timeout=10
+ )
+ main_response.raise_for_status()
+
+ mention_response = requests.post(
+ DISCORD_WEBHOOK_URL_REPORT,
+ json={"content": "<@&1198482895342411846>"},
+ timeout=10
+ )
+ mention_response.raise_for_status()
+
+ except requests.exceptions.RequestException as exception:
+ print(f"Error sending Discord message: {exception}")
+ except Exception as exception:
+ print(f"Unexpected error: {exception}")
+
+
def frogpilot_boot_functions(build_metadata, params):
params_memory = Params(memory=True)
diff --git a/frogpilot/frogpilot_process.py b/frogpilot/frogpilot_process.py
index 6fe11a12..325d35a2 100644
--- a/frogpilot/frogpilot_process.py
+++ b/frogpilot/frogpilot_process.py
@@ -10,7 +10,7 @@ from openpilot.common.time_helpers import system_time_valid
from openpilot.frogpilot.assets.theme_manager import THEME_COMPONENT_PARAMS, ThemeManager
from openpilot.frogpilot.common.frogpilot_backups import backup_toggles
-from openpilot.frogpilot.common.frogpilot_functions import update_maps, update_openpilot
+from openpilot.frogpilot.common.frogpilot_functions import capture_report, update_maps, update_openpilot
from openpilot.frogpilot.common.frogpilot_utilities import ThreadManager, flash_panda, is_url_pingable, lock_doors
from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, FrogPilotVariables
from openpilot.frogpilot.controls.frogpilot_planner import FrogPilotPlanner
@@ -28,6 +28,11 @@ def check_assets(theme_manager, thread_manager, params_memory, frogpilot_toggles
if params_memory.get_bool("FlashPanda"):
thread_manager.run_with_lock(flash_panda, (params_memory))
+ report_data = params_memory.get("IssueReported")
+ if report_data:
+ capture_report(report_data["DiscordUser"], report_data["Issue"], vars(frogpilot_toggles))
+ params_memory.remove("IssueReported")
+
def transition_offroad(frogpilot_planner, theme_manager, thread_manager, time_validated, sm, params, frogpilot_toggles):
params.put("LastGPSPosition", json.dumps(frogpilot_planner.gps_position))
diff --git a/frogpilot/ui/qt/offroad/utilities.cc b/frogpilot/ui/qt/offroad/utilities.cc
index 1d74b61a..e74f77a1 100644
--- a/frogpilot/ui/qt/offroad/utilities.cc
+++ b/frogpilot/ui/qt/offroad/utilities.cc
@@ -68,6 +68,58 @@ FrogPilotUtilitiesPanel::FrogPilotUtilitiesPanel(FrogPilotSettingsWindow *parent
}
addItem(forceStartedButton);
+ ButtonControl *reportIssueButton = new ButtonControl(tr("Report a Bug or an Issue"), tr("REPORT"), tr("Send a bug report so we can help fix the problem!"));
+ QObject::connect(reportIssueButton, &ButtonControl::clicked, [this]() {
+ if (!frogpilotUIState()->frogpilot_scene.online) {
+ ConfirmationDialog::alert(tr("Please connect to the internet before sending a report!"), this);
+ return;
+ }
+
+ QStringList report_messages = {
+ tr("Acceleration feels harsh or jerky"),
+ tr("An alert was unclear and I'm not sure what it meant"),
+ tr("Braking is too sudden or uncomfortable"),
+ tr("I'm not sure if this is normal or a bug:"),
+ tr("My steering wheel buttons aren't working"),
+ tr("openpilot disengages when I don't expect it"),
+ tr("openpilot feels sluggish or slow to respond"),
+ tr("Something else (please describe)")
+ };
+
+ if (QFile::exists("/data/error_logs/error.txt")) {
+ report_messages.prepend(tr("I saw an alert that said \"openpilot crashed\""));
+ }
+
+ QString selected_issue = MultiOptionDialog::getSelection(tr("What's going on?"), report_messages, "", this);
+ if (selected_issue.isEmpty()) {
+ return;
+ }
+
+ if (selected_issue.contains("crashed") || selected_issue.contains("not sure") || selected_issue.contains("Something else")) {
+ QString extra_input = InputDialog::getText(tr("Please describe what's happening"), this, tr("Send Report"), false, 10, "", 300).trimmed();
+ if (extra_input.isEmpty()) {
+ return;
+ }
+ selected_issue += " — " + extra_input;
+ }
+
+ QString discord_user = InputDialog::getText(tr("What's your Discord username?"), this, tr("Send Report"), false, -1, QString::fromStdString(params.get("DiscordUsername"))).trimmed();
+
+ QJsonObject reportData;
+ reportData["DiscordUser"] = discord_user;
+ reportData["Issue"] = selected_issue;
+
+ params.putNonBlocking("DiscordUsername", discord_user.toStdString());
+ params_memory.put("IssueReported", QJsonDocument(reportData).toJson(QJsonDocument::Compact).toStdString());
+
+ ConfirmationDialog::alert(tr("Report Sent! Thanks for letting us know!"), this);
+ });
+ if (forceOpenDescriptions) {
+ reportIssueButton->showDescription();
+ }
+ addItem(reportIssueButton);
+ reportIssueButton->setVisible(QString::fromStdString(params.get("GitRemote")).toLower() == "https://github.com/frogai/openpilot.git");
+
ButtonControl *resetTogglesButton = new ButtonControl(tr("Reset Toggles to Default"), tr("RESET"), tr("Reset all toggles to their default values."));
QObject::connect(resetTogglesButton, &ButtonControl::clicked, [parent, resetTogglesButton, this]() {
if (ConfirmationDialog::confirm(tr("Are you sure you want to reset all toggles to their default values?"), tr("Reset"), this)) {