diff --git a/common/api.py b/common/api.py index ebf0290d..22fb7f65 100644 --- a/common/api.py +++ b/common/api.py @@ -5,7 +5,9 @@ from datetime import datetime, timedelta, UTC from openpilot.system.hardware.hw import Paths from openpilot.system.version import get_version -API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') +from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server + +API_HOST = os.getenv('API_HOST', f'https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}') # name: jwt signature algorithm KEYS = {"id_rsa": "RS256", diff --git a/frogpilot/common/frogpilot_functions.py b/frogpilot/common/frogpilot_functions.py index ff79dd12..b3015380 100644 --- a/frogpilot/common/frogpilot_functions.py +++ b/frogpilot/common/frogpilot_functions.py @@ -9,13 +9,14 @@ from pathlib import Path from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid +from openpilot.system.athena.registration import register from openpilot.system.hardware import HARDWARE 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_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, THEME_SAVE_PATH, + ERROR_LOGS_PATH, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, THEME_SAVE_PATH, FrogPilotVariables, get_frogpilot_toggles ) @@ -26,6 +27,15 @@ def frogpilot_boot_functions(build_metadata, params): FrogPilotVariables() ThemeManager(params, params_memory, boot_run=True).update_active_theme(time_validated=system_time_valid(), frogpilot_toggles=get_frogpilot_toggles(), boot_run=True) + if use_konik_server(): + if params.get("KonikDongleId") is not None: + params.put("DongleId", params.get("KonikDongleId")) + else: + params.put("KonikDongleId", register(show_spinner=True, register_konik=True)) + params.put("DongleId", params.get("KonikDongleId")) + elif params.get("DongleId") == params.get("KonikDongleId"): + params.put("DongleId", params.get("StockDongleId")) + def boot_thread(): while not system_time_valid(): print("Waiting for system time to become valid...") @@ -40,6 +50,7 @@ def install_frogpilot(build_metadata, params): paths = [ ERROR_LOGS_PATH, HD_LOGS_PATH, + KONIK_LOGS_PATH, THEME_SAVE_PATH ] for path in paths: diff --git a/frogpilot/common/frogpilot_utilities.py b/frogpilot/common/frogpilot_utilities.py index ccdd4d8e..8e052426 100644 --- a/frogpilot/common/frogpilot_utilities.py +++ b/frogpilot/common/frogpilot_utilities.py @@ -9,6 +9,7 @@ import threading import time import zipfile +from functools import cache from pathlib import Path import openpilot.system.sentry as sentry @@ -17,7 +18,7 @@ from cereal import log, messaging from openpilot.common.realtime import DT_DMON, DT_HW from panda import Panda -from openpilot.frogpilot.common.frogpilot_variables import EARTH_RADIUS, FROGS_GO_MOO_PATH +from openpilot.frogpilot.common.frogpilot_variables import EARTH_RADIUS, FROGS_GO_MOO_PATH, KONIK_PATH class ThreadManager: def __init__(self): @@ -211,6 +212,11 @@ def update_json_file(path, data): os.replace(temp_path, path) +@cache +def use_konik_server(): + return KONIK_PATH.is_file() + + def wait_for_no_driver(params, sm, time_threshold=60): while sm["deviceState"].screenBrightnessPercent != 0 or any(proc.name == "dmonitoringd" and proc.running for proc in sm["managerState"].processes): sm.update() diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py index 7a6482f5..dd0917dd 100644 --- a/frogpilot/common/frogpilot_variables.py +++ b/frogpilot/common/frogpilot_variables.py @@ -67,6 +67,9 @@ FROGS_GO_MOO_PATH = Path("/persist/frogsgomoo.py") HD_LOGS_PATH = Path("/data/media/0/realdata_HD") HD_PATH = Path("/cache/use_HD") +KONIK_LOGS_PATH = Path("/data/media/0/realdata_konik") +KONIK_PATH = Path("/cache/use_konik") + MAPD_PATH = Path("/data/media/0/osm/mapd") MAPS_PATH = Path("/data/media/0/osm/offline") @@ -225,6 +228,17 @@ class FrogPilotVariables: HD_PATH.unlink() HARDWARE.reboot() + toggle.use_konik_server = device_management + toggle.use_konik_server &= self.get_value("UseKonikServer") + toggle.use_konik_server |= Path("/data/openpilot/not_vetted").is_file() + + if not KONIK_PATH.is_file() and toggle.use_konik_server: + KONIK_PATH.touch() + HARDWARE.reboot() + elif KONIK_PATH.is_file() and not toggle.use_konik_server: + KONIK_PATH.unlink() + HARDWARE.reboot() + stock_colors_json = (STOCK_THEME_PATH / "colors/colors.json") self.stock_colors = json.loads(stock_colors_json.read_text()) if stock_colors_json.is_file() else {} diff --git a/frogpilot/ui/qt/offroad/device_settings.cc b/frogpilot/ui/qt/offroad/device_settings.cc index 0c957e6a..ee91cd78 100644 --- a/frogpilot/ui/qt/offroad/device_settings.cc +++ b/frogpilot/ui/qt/offroad/device_settings.cc @@ -29,6 +29,7 @@ FrogPilotDevicePanel::FrogPilotDevicePanel(FrogPilotSettingsWindow *parent, bool {"HigherBitrate", tr("High-Quality Recording"), tr("Save drive footage in higher video quality."), ""}, {"LowVoltageShutdown", tr("Low-Voltage Cutoff"), tr("While parked, if the battery voltage falls below the set level, the device shuts down to prevent excessive battery drain."), ""}, {"IncreaseThermalLimits", tr("Raise Temperature Limits"), QString("%1

%2").arg(tr("WARNING: Running at higher temperatures may damage your device!")).arg(tr("Allow the device to run at higher temperatures before throttling or shutting down. Use only if you understand the risks!")), ""}, + {"UseKonikServer", tr("Use Konik Server"), tr("Upload driving data to \"stable.konik.ai\" instead of \"connect.comma.ai\"."), ""}, {"ScreenManagement", tr("Screen Settings"), tr("Settings that control screen brightness, screen recording, and timeout duration."), "../../frogpilot/assets/toggle_icons/icon_light.png"}, {"ScreenBrightness", tr("Screen Brightness (Offroad)"), tr("The screen brightness while not driving."), ""}, @@ -136,12 +137,14 @@ FrogPilotDevicePanel::FrogPilotDevicePanel(FrogPilotSettingsWindow *parent, bool QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, this, &FrogPilotDevicePanel::updateToggles); } - QSet rebootKeys = {"HigherBitrate"}; + QSet rebootKeys = {"HigherBitrate", "UseKonikServer"}; for (const QString &key : rebootKeys) { QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, [key, this](bool state) { QString filePath; if (key == "HigherBitrate") { filePath = "/cache/use_HD"; + } else if (key == "UseKonikServer") { + filePath = "/cache/use_konik"; } if (!filePath.isEmpty()) { @@ -203,6 +206,10 @@ void FrogPilotDevicePanel::updateToggles() { setVisible &= params.getBool("DeviceManagement") && params.getBool("NoUploads") && !params.getBool("DisableOnroadUploads"); } + else if (key == "UseKonikServer" && QFile("/data/not_vetted").exists()) { + static_cast(toggle)->forceOn(true); + } + toggle->setVisible(setVisible); if (setVisible) { diff --git a/frogpilot/ui/qt/offroad/device_settings.h b/frogpilot/ui/qt/offroad/device_settings.h index 7a281bae..d1577179 100644 --- a/frogpilot/ui/qt/offroad/device_settings.h +++ b/frogpilot/ui/qt/offroad/device_settings.h @@ -23,7 +23,7 @@ private: std::map toggles; - QSet deviceManagementKeys = {"DeviceShutdown", "HigherBitrate", "IncreaseThermalLimits", "LowVoltageShutdown", "NoLogging", "NoUploads"}; + QSet deviceManagementKeys = {"DeviceShutdown", "HigherBitrate", "IncreaseThermalLimits", "LowVoltageShutdown", "NoLogging", "NoUploads", "UseKonikServer"}; QSet screenKeys = {"ScreenBrightness", "ScreenBrightnessOnroad", "ScreenRecorder", "ScreenTimeout", "ScreenTimeoutOnroad", "StandbyMode"}; QSet parentKeys; diff --git a/frogpilot/ui/qt/widgets/drive_stats.cc b/frogpilot/ui/qt/widgets/drive_stats.cc index 208d6f29..f52142ee 100644 --- a/frogpilot/ui/qt/widgets/drive_stats.cc +++ b/frogpilot/ui/qt/widgets/drive_stats.cc @@ -10,12 +10,13 @@ static QLabel *newLabel(const QString &text, const QString &type) { DriveStats::DriveStats(QWidget *parent) : QFrame(parent) { isMetric = params.getBool("IsMetric"); + konik = useKonikServer(); QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(50, 25, 50, 20); - addStatsLayouts(tr("ALL TIME"), all); - addStatsLayouts(tr("PAST WEEK"), week); + addStatsLayouts(konik ? tr("ALL TIME (KONIK)") : tr("ALL TIME"), all); + addStatsLayouts(konik ? tr("PAST WEEK (KONIK)") : tr("PAST WEEK"), week); addStatsLayouts(tr("FROGPILOT"), frogPilot, true); std::optional dongleId = getDongleId(); @@ -103,5 +104,5 @@ void DriveStats::updateStats() { updateStatsForLabel(json["week"].toObject(), week); updateFrogPilotStatsForLabel(frogPilot); - params.putIntNonBlocking("openpilotMinutes", json["all"].toObject()["minutes"].toDouble()); + params.putIntNonBlocking(konik ? "KonikMinutes" : "openpilotMinutes", json["all"].toObject()["minutes"].toDouble()); } diff --git a/frogpilot/ui/qt/widgets/drive_stats.h b/frogpilot/ui/qt/widgets/drive_stats.h index a7b89d72..d9692998 100644 --- a/frogpilot/ui/qt/widgets/drive_stats.h +++ b/frogpilot/ui/qt/widgets/drive_stats.h @@ -23,6 +23,7 @@ private: void updateStatsForLabel(const QJsonObject &obj, StatsLabels &labels); bool isMetric; + bool konik; Params params; diff --git a/frogpilot/ui/qt/widgets/frogpilot_controls.cc b/frogpilot/ui/qt/widgets/frogpilot_controls.cc index ec2f6de4..d2757072 100644 --- a/frogpilot/ui/qt/widgets/frogpilot_controls.cc +++ b/frogpilot/ui/qt/widgets/frogpilot_controls.cc @@ -17,6 +17,11 @@ bool isFrogsGoMoo() { return is_FrogsGoMoo; } +bool useKonikServer() { + static bool use_konik = QFile::exists("/cache/use_konik"); + return use_konik; +} + void clearMovie(QSharedPointer &movie, QWidget *parent) { if (!movie) { return; diff --git a/frogpilot/ui/qt/widgets/frogpilot_controls.h b/frogpilot/ui/qt/widgets/frogpilot_controls.h index 8f720e8c..0fcc4405 100644 --- a/frogpilot/ui/qt/widgets/frogpilot_controls.h +++ b/frogpilot/ui/qt/widgets/frogpilot_controls.h @@ -16,6 +16,7 @@ #include "selfdrive/ui/qt/widgets/controls.h" bool isFrogsGoMoo(); +bool useKonikServer(); void loadGif(const QString &gifPath, QSharedPointer &movie, const QSize &size, QWidget *parent); void loadImage(const QString &basePath, QPixmap &pixmap, QSharedPointer &movie, const QSize &size, QWidget *parent); diff --git a/selfdrive/ui/qt/api.h b/selfdrive/ui/qt/api.h index ad64d7e7..e4dcc102 100644 --- a/selfdrive/ui/qt/api.h +++ b/selfdrive/ui/qt/api.h @@ -7,9 +7,11 @@ #include "common/util.h" +#include "frogpilot/ui/qt/widgets/frogpilot_controls.h" + namespace CommaApi { -const QString BASE_URL = util::getenv("API_HOST", "https://api.commadotai.com").c_str(); +const QString BASE_URL = util::getenv("API_HOST", useKonikServer() ? "https://api.konik.ai" : "https://api.commadotai.com").c_str(); QByteArray rsa_sign(const QByteArray &data); QString create_jwt(const QJsonObject &payloads = {}, int expiry = 3600); diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index f819561c..23beba9b 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -229,7 +229,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); pair_device = new ButtonControl(tr("Pair Device"), tr("PAIR"), - tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); + useKonikServer() ? tr("Pair your device with Konik connect (stable.konik.ai).") : tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); connect(pair_device, &ButtonControl::clicked, [=]() { PairingPopup popup(this); popup.exec(); @@ -577,7 +577,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { if (!tuningLevelConfirmed) { int frogpilotHours = QJsonDocument::fromJson(QString::fromStdString(params.get("FrogPilotStats")).toUtf8()).object().value("FrogPilotSeconds").toInt() / (60 * 60); - int openpilotHours = params.getInt("openpilotMinutes") / 60; + int openpilotHours = params.getInt("KonikMinutes") / 60 + params.getInt("openpilotMinutes") / 60; if (frogpilotHours < 1 && openpilotHours < 100) { if (openpilotHours < 10) { diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index ee820c46..1a28ea69 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -36,7 +36,7 @@ void PairingQRWidget::hideEvent(QHideEvent *event) { void PairingQRWidget::refresh() { QString pairToken = CommaApi::create_jwt({{"pair", true}}); - QString qrString = "https://connect.comma.ai/?pair=" + pairToken; + QString qrString = (useKonikServer() ? "https://stable.konik.ai/?pair=" : "https://connect.comma.ai/?pair=") + pairToken; this->updateQrCode(qrString); update(); } @@ -89,7 +89,7 @@ PairingPopup::PairingPopup(QWidget *parent) : DialogBase(parent) { vlayout->addSpacing(30); - QLabel *title = new QLabel(tr("Pair your device to your comma account"), this); + QLabel *title = new QLabel(tr("Pair your device to your %1 account").arg(useKonikServer() ? "Konik" : "comma"), this); title->setStyleSheet("font-size: 75px; color: black;"); title->setWordWrap(true); vlayout->addWidget(title); @@ -100,9 +100,9 @@ PairingPopup::PairingPopup(QWidget *parent) : DialogBase(parent) {
  • %2
  • %3
  • - )").arg(tr("Go to https://connect.comma.ai on your phone")) + )").arg(tr("Go to https://%1 on your phone").arg(useKonikServer() ? "stable.konik.ai" : "connect.comma.ai")) .arg(tr("Click \"add new device\" and scan the QR code on the right")) - .arg(tr("Bookmark connect.comma.ai to your home screen to use it like an app")), this); + .arg(tr("Bookmark %1 to your home screen to use it like an app").arg(useKonikServer() ? "stable.konik.ai" : "connect.comma.ai")), this); instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); instructions->setWordWrap(true); @@ -196,7 +196,7 @@ SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold;"); finishRegistrationLayout->addWidget(registrationTitle); - QLabel* registrationDescription = new QLabel(tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); + QLabel* registrationDescription = new QLabel(useKonikServer() ? tr("Pair your device with Konik connect (stable.konik.ai).") : tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); registrationDescription->setWordWrap(true); registrationDescription->setStyleSheet("font-size: 50px; font-weight: light;"); finishRegistrationLayout->addWidget(registrationDescription); diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 3b71a9c3..46683a9b 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -40,8 +40,10 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_build_metadata from openpilot.system.hardware.hw import Paths +from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server -ATHENA_HOST = os.getenv('ATHENA_HOST', 'wss://athena.comma.ai') + +ATHENA_HOST = os.getenv('ATHENA_HOST', f"wss://athena.{'konik.ai' if use_konik_server() else 'comma.ai'}") HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) LOCAL_PORT_WHITELIST = {22, } # SSH diff --git a/system/athena/registration.py b/system/athena/registration.py index 5df906cf..d20de70b 100644 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -24,7 +24,7 @@ def is_registered_device() -> bool: return dongle not in (None, UNREGISTERED_DONGLE_ID) -def register(show_spinner=False) -> str | None: +def register(show_spinner=False, register_konik=False) -> str | None: """ All devices built since March 2024 come with all info stored in /persist/. This is kept around @@ -45,10 +45,10 @@ def register(show_spinner=False) -> str | None: # Create registration token, in the future, this key will make JWTs directly jwt_algo, private_key, public_key = get_key_pair() - if not public_key: + if not public_key and not register_konik: dongle_id = UNREGISTERED_DONGLE_ID cloudlog.warning("missing public key") - elif dongle_id is None: + elif dongle_id is None or register_konik: if show_spinner: spinner = Spinner() spinner.update("registering device") @@ -96,8 +96,9 @@ def register(show_spinner=False) -> str | None: if show_spinner: spinner.close() - if dongle_id: + if not register_konik and dongle_id != params.get("KonikDongleId"): params.put("DongleId", dongle_id) + params.put("StockDongleId", dongle_id) set_offroad_alert("Offroad_UnregisteredHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC) return dongle_id diff --git a/system/hardware/hw.h b/system/hardware/hw.h index 865a5261..1154a7ea 100644 --- a/system/hardware/hw.h +++ b/system/hardware/hw.h @@ -37,6 +37,10 @@ namespace Path { return "/data/media/0/realdata_HD/"; } + if (std::filesystem::exists("/cache/use_konik")) { + return "/data/media/0/realdata_konik/"; + } + return "/data/media/0/realdata"; } diff --git a/system/hardware/hw.py b/system/hardware/hw.py index f962de20..a726cc1b 100644 --- a/system/hardware/hw.py +++ b/system/hardware/hw.py @@ -19,6 +19,8 @@ class Paths: return str(Path(Paths.comma_home()) / "media" / "0" / "realdata") elif Path("/cache/use_HD").is_file(): return '/data/media/0/realdata_HD/' + elif Path("/cache/use_konik").is_file(): + return '/data/media/0/realdata_konik/' else: return '/data/media/0/realdata/' diff --git a/tools/lib/api.py b/tools/lib/api.py index c6e2d989..2780481d 100644 --- a/tools/lib/api.py +++ b/tools/lib/api.py @@ -1,6 +1,9 @@ import os import requests -API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') + +from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server + +API_HOST = os.getenv('API_HOST', f'https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}') # TODO: this should be merged into common.api diff --git a/tools/lib/auth.py b/tools/lib/auth.py index 5988397d..39a9e04a 100755 --- a/tools/lib/auth.py +++ b/tools/lib/auth.py @@ -22,6 +22,7 @@ Examples:: """ import argparse +import os import sys import pprint import webbrowser @@ -32,6 +33,9 @@ from urllib.parse import parse_qs, urlencode from openpilot.tools.lib.api import APIError, CommaApi, UnauthorizedError from openpilot.tools.lib.auth_config import set_token, get_token +from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server + +API_HOST = os.getenv('API_HOST', f'https://api.{'konik' if use_konik_server() else 'comma'}.ai') PORT = 3000 @@ -66,7 +70,7 @@ def auth_redirect_link(method): }[method] params = { - 'redirect_uri': f"https://api.comma.ai/v2/auth/{provider_id}/redirect/", + 'redirect_uri': f"{API_HOST}/v2/auth/{provider_id}/redirect/", 'state': f'service,localhost:{PORT}', } diff --git a/tools/scripts/fetch_image_from_route.py b/tools/scripts/fetch_image_from_route.py index 77bf48f9..c1dbe4f7 100755 --- a/tools/scripts/fetch_image_from_route.py +++ b/tools/scripts/fetch_image_from_route.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import sys +from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server + if len(sys.argv) < 4: print(f"{sys.argv[0]} [front|wide|driver]") print('example: ./fetch_image_from_route.py "02c45f73a2e5c6e9|2020-06-01--18-03-08" 3 500 driver') @@ -24,7 +26,7 @@ segment = int(sys.argv[2]) frame = int(sys.argv[3]) camera = cameras[sys.argv[4]] if len(sys.argv) > 4 and sys.argv[4] in cameras else "cameras" -url = f'https://api.commadotai.com/v1/route/{route}/files' +url = f'https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}/v1/route/{route}/files' r = requests.get(url, headers={"Authorization": f"JWT {jwt}"}, timeout=10) assert r.status_code == 200 print("got api response")