From 1457b902acee18ca92b8dc8eff3e6b5b35e07723 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:17:18 -0500 Subject: [PATCH] Warn of Agnos --- starpilot/system/speed_limit_vision.py | 51 ++-- .../components/tools/update_manager.css | 23 ++ .../assets/components/tools/update_manager.js | 154 ++++++++++- .../tests/test_update_agnos_warning.py | 121 +++++++++ starpilot/system/the_galaxy/the_galaxy.py | 241 +++++++++++++++++- 5 files changed, 562 insertions(+), 28 deletions(-) create mode 100644 starpilot/system/the_galaxy/tests/test_update_agnos_warning.py diff --git a/starpilot/system/speed_limit_vision.py b/starpilot/system/speed_limit_vision.py index 969d4cca3..29e5fb107 100644 --- a/starpilot/system/speed_limit_vision.py +++ b/starpilot/system/speed_limit_vision.py @@ -55,7 +55,7 @@ MAP_NEXT_REVIEW_DISTANCE_METERS = 120.0 MAP_TRANSITION_MISS_CAPTURE_COOLDOWN_SECONDS = 8.0 MAP_VISION_MATCH_WINDOW_SECONDS = 2.5 MODEL_PROPOSAL_MIN_CONFIDENCE = 0.0001 -MODEL_PROPOSAL_MAX_COUNT = 16 +MODEL_PROPOSAL_MAX_COUNT = 4 MODEL_PROPOSAL_MAX_AREA_RATIO = 0.18 MODEL_PROPOSAL_MIN_WIDTH = 10 MODEL_PROPOSAL_MIN_HEIGHT = 18 @@ -152,6 +152,7 @@ SCHOOL_ZONE_SPEED_VALUES = frozenset((15, 20, 25)) US_DETECTOR_MIN_CONFIDENCE = 0.10 US_CLASSIFIER_MIN_CONFIDENCE = 0.50 US_CLASSIFIER_REJECT_MIN_CONFIDENCE = 0.85 +SEPARATE_REJECT_CLASSIFIER_ENABLED = False US_REJECT_CLASSIFIER_MIN_CONFIDENCE = 0.85 DETECTOR_CLASSIFIER_EXPANSIONS = ( (0.00, 0.00, 0.00, 0.00, 1.10), @@ -180,7 +181,7 @@ DETECTOR_CLASSIFIER_MIN_ACCEPT_HEIGHT = 40 DETECTOR_CLASSIFIER_RESCUE_MIN_WIDTH = 14 DETECTOR_CLASSIFIER_RESCUE_MIN_HEIGHT = 18 DETECTOR_CLASSIFIER_RESCUE_MIN_X_RATIO = 0.52 -DETECTOR_CLASSIFIER_RESCUE_MIN_SUPPORT = 1 +DETECTOR_CLASSIFIER_RESCUE_MIN_SUPPORT = 2 DETECTOR_CLASSIFIER_RESCUE_MIN_CONFIDENCE = 0.90 DETECTOR_CLASSIFIER_RESCUE_MAX_SCORE = 0.64 DETECTOR_CLASSIFIER_TRUSTED_MODEL_MAX_HEIGHT = 55 @@ -189,6 +190,8 @@ DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_PROPOSAL_CONFIDENCE = 0.18 DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_X_RATIO = 0.52 DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_READ_CONFIDENCE = 0.98 DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_SUPPORT = 2 +DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE = 0.70 +DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_READ_CONFIDENCE = 0.995 SCHOOL_ZONE_SPEED_PRIOR = 0.12 SCHOOL_ZONE_SUPPORT_BONUS = 0.08 SCHOOL_ZONE_MIN_SUPPORT = 2 @@ -800,7 +803,7 @@ class SpeedLimitVisionDaemon: self.classifier_net = cv2.dnn.readNetFromONNX(str(US_CLASSIFIER_MODEL_PATH)) self.classifier_net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) self.classifier_net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) - if US_REJECT_CLASSIFIER_MODEL_PATH.is_file(): + if SEPARATE_REJECT_CLASSIFIER_ENABLED and US_REJECT_CLASSIFIER_MODEL_PATH.is_file(): try: self.reject_classifier_net = cv2.dnn.readNetFromONNX(str(US_REJECT_CLASSIFIER_MODEL_PATH)) self.reject_classifier_net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) @@ -1276,19 +1279,20 @@ class SpeedLimitVisionDaemon: if self.classifier_net is None or sign_crop.size == 0: return None - normalized_mask = self._extract_value_template_mask(sign_crop) speed_class_count = len(US_CLASSIFIER_SPEED_VALUES) - if normalized_mask is not None and self.reject_classifier_net is not None: - reject_input = cv2.cvtColor(normalized_mask, cv2.COLOR_GRAY2BGR) - reject_crop = self._square_resize(reject_input, size=128) - reject_blob = cv2.dnn.blobFromImage(reject_crop, scalefactor=1 / 255.0, size=(128, 128), swapRB=True, crop=False) - self.reject_classifier_net.setInput(reject_blob) - reject_scores = np.array(self.reject_classifier_net.forward()).reshape(-1) - if reject_scores.size == speed_class_count + 1: - reject_probabilities = self._normalize_classifier_output(reject_scores) - if float(reject_probabilities[speed_class_count]) >= US_REJECT_CLASSIFIER_MIN_CONFIDENCE: - return None + if self.reject_classifier_net is not None: + normalized_mask = self._extract_value_template_mask(sign_crop) + if normalized_mask is not None: + reject_input = cv2.cvtColor(normalized_mask, cv2.COLOR_GRAY2BGR) + reject_crop = self._square_resize(reject_input, size=128) + reject_blob = cv2.dnn.blobFromImage(reject_crop, scalefactor=1 / 255.0, size=(128, 128), swapRB=True, crop=False) + self.reject_classifier_net.setInput(reject_blob) + reject_scores = np.array(self.reject_classifier_net.forward()).reshape(-1) + if reject_scores.size == speed_class_count + 1: + reject_probabilities = self._normalize_classifier_output(reject_scores) + if float(reject_probabilities[speed_class_count]) >= US_REJECT_CLASSIFIER_MIN_CONFIDENCE: + return None padded_crop = self._square_resize(sign_crop, size=128) blob = cv2.dnn.blobFromImage(padded_crop, scalefactor=1 / 255.0, size=(128, 128), swapRB=True, crop=False) @@ -1427,7 +1431,19 @@ class SpeedLimitVisionDaemon: proposal_confidence >= DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_PROPOSAL_CONFIDENCE and model_read[1] >= DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_READ_CONFIDENCE ) - needs_ocr_confirmation = class_id != 2 and (not is_regulatory or is_tiny_low_conf_box) and not trusted_model_read + strong_model_read = ( + class_id == 0 and + model_read is not None and + not is_small_box and + proposal_confidence >= DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE and + model_read[1] >= DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_READ_CONFIDENCE + ) + needs_ocr_confirmation = ( + class_id != 2 and + (not is_regulatory or is_tiny_low_conf_box) and + not trusted_model_read and + not strong_model_read + ) if model_read is None or needs_ocr_confirmation: ocr_read = self._read_speed_limit_from_crop(sign_crop) read_result = model_read or ocr_read @@ -1440,7 +1456,7 @@ class SpeedLimitVisionDaemon: read_result = (model_read[0], min(model_read[1], ocr_read[1])) speed_limit_mph, read_confidence = read_result - score_is_regulatory = is_regulatory or trusted_model_read + score_is_regulatory = is_regulatory or trusted_model_read or strong_model_read if ( class_id == 2 and proposal_confidence < SCHOOL_ZONE_FALLBACK_MIN_CONFIDENCE and @@ -1459,7 +1475,7 @@ class SpeedLimitVisionDaemon: speed_scores[speed_limit_mph] = speed_scores.get(speed_limit_mph, 0.0) + score speed_best_confidences[speed_limit_mph] = max(speed_best_confidences.get(speed_limit_mph, 0.0), read_confidence) speed_support_counts[speed_limit_mph] = speed_support_counts.get(speed_limit_mph, 0) + 1 - if is_regulatory or class_id == 2: + if is_regulatory or class_id == 2 or strong_model_read: speed_regulatory_support[speed_limit_mph] = speed_regulatory_support.get(speed_limit_mph, 0) + 1 if trusted_model_read: speed_trusted_model_support[speed_limit_mph] = speed_trusted_model_support.get(speed_limit_mph, 0) + 1 @@ -1900,6 +1916,7 @@ class SpeedLimitVisionDaemon: "modelMode": self.model_mode, "detectorInputSize": self.detector_input_size, "detectorRegionMode": DETECTOR_CLASSIFIER_REGION_MODE, + "separateRejectClassifierEnabled": SEPARATE_REJECT_CLASSIFIER_ENABLED, "stream": self.stream_name, "cameraConnected": camera_connected, "debugSession": self.debug_session_id, diff --git a/starpilot/system/the_galaxy/assets/components/tools/update_manager.css b/starpilot/system/the_galaxy/assets/components/tools/update_manager.css index b504b5927..36ebe74ed 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/update_manager.css +++ b/starpilot/system/the_galaxy/assets/components/tools/update_manager.css @@ -127,6 +127,29 @@ margin-top: var(--margin-base); } +.updateAgnosWarning { + background: rgba(212, 160, 96, 0.12); + border-left: 3px solid var(--warning-bg); + border-radius: var(--border-radius-sm); + margin-top: var(--margin-base); + padding: var(--padding-sm) var(--padding-base); +} + +.updateAgnosWarning p { + font-size: var(--font-size-sm); + margin: var(--margin-xs) 0; +} + +.updateAgnosWarning ul { + font-size: var(--font-size-sm); + margin: var(--margin-sm) 0; + padding-left: 1.2rem; +} + +.updateAgnosWarning li { + margin-bottom: var(--margin-xs); +} + .updateHint { font-size: var(--font-size-sm); margin-top: var(--margin-sm); diff --git a/starpilot/system/the_galaxy/assets/components/tools/update_manager.js b/starpilot/system/the_galaxy/assets/components/tools/update_manager.js index c0b3ca19d..c027a2de8 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/update_manager.js +++ b/starpilot/system/the_galaxy/assets/components/tools/update_manager.js @@ -17,6 +17,10 @@ const state = reactive({ switchBusy: false, rollbackBusy: false, recoveryBusy: false, + selectedBranchAgnosUpdate: null, + selectedBranchAgnosBusy: false, + selectedBranchAgnosTarget: "", + selectedBranchAgnosError: "", }) let initialized = false @@ -57,6 +61,52 @@ function isSelectedBranchDifferent() { return !!currentBranch && !!selectedBranch && currentBranch !== selectedBranch } +function clearSelectedBranchAgnosStatus() { + state.selectedBranchAgnosUpdate = null + state.selectedBranchAgnosBusy = false + state.selectedBranchAgnosTarget = "" + state.selectedBranchAgnosError = "" +} + +function activeAgnosUpdate() { + if (isSelectedBranchDifferent()) { + return state.selectedBranchAgnosUpdate || null + } + return state.status?.agnosUpdate || null +} + +function activeAgnosError() { + if (isSelectedBranchDifferent()) { + return state.selectedBranchAgnosError || "" + } + return state.status?.agnosUpdate?.error || "" +} + +function agnosWarningItems(update = activeAgnosUpdate()) { + const warnings = Array.isArray(update?.warnings) ? update.warnings.filter(Boolean) : [] + if (warnings.length) return warnings + return [ + "This AGNOS firmware update will take much longer than a normal software update.", + "You must be able to physically access the device to press the on-device update button.", + "It downloads about 900 MB of data, so Wi-Fi is recommended.", + ] +} + +function agnosChangedPartitionsText(update = activeAgnosUpdate()) { + const partitions = Array.isArray(update?.changedPartitions) ? update.changedPartitions.filter(Boolean) : [] + if (!partitions.length) return "" + return `Changed AGNOS partitions: ${partitions.join(", ")}` +} + +function agnosConfirmationText(update = activeAgnosUpdate()) { + if (!update?.available) return "" + + const warningLines = agnosWarningItems(update) + .map((warning) => `- ${warning}`) + .join("\n") + return `\n\nAGNOS firmware update included:\n\n${warningLines}` +} + function normalizeGithubRemote(remoteValue, commitsUrlValue = "") { let remote = String(remoteValue || "").trim() if (!remote) { @@ -250,6 +300,12 @@ async function fetchStatus(showToast) { state.selectedBranch = payload.branch } + if (isSelectedBranchDifferent()) { + fetchSelectedBranchAgnosStatus(false) + } else { + clearSelectedBranchAgnosStatus() + } + if (shouldContinuePolling()) { ensurePolling() } else { @@ -258,7 +314,10 @@ async function fetchStatus(showToast) { if (showToast) { state.checkedForUpdates = true - showSnackbar(payload.updateAvailable ? "Update available." : "No update available.") + const updateMessage = payload.updateAvailable + ? (payload.agnosUpdate?.available ? "Update available. AGNOS firmware update included." : "Update available.") + : "No update available." + showSnackbar(updateMessage) } } catch (error) { const isRebootTransitionError = !showToast @@ -312,6 +371,12 @@ async function fetchBranches(showToast = false) { state.hasManualBranchSelection = false } + if (isSelectedBranchDifferent()) { + fetchSelectedBranchAgnosStatus(false) + } else { + clearSelectedBranchAgnosStatus() + } + if (showToast) { showSnackbar(branches.length ? `Loaded ${branches.length} branches.` : "No branches found.") } @@ -326,6 +391,56 @@ async function fetchBranches(showToast = false) { } } +async function fetchSelectedBranchAgnosStatus(showToast = false, force = false) { + const targetBranch = String(state.selectedBranch || "").trim() + if (!targetBranch || !isSelectedBranchDifferent() || state.status?.running) { + clearSelectedBranchAgnosStatus() + return null + } + + if (!force && state.selectedBranchAgnosUpdate?.targetBranch === targetBranch) { + return state.selectedBranchAgnosUpdate + } + if (state.selectedBranchAgnosBusy && state.selectedBranchAgnosTarget === targetBranch) { + return state.selectedBranchAgnosUpdate + } + + state.selectedBranchAgnosBusy = true + state.selectedBranchAgnosTarget = targetBranch + state.selectedBranchAgnosError = "" + try { + const response = await fetch(`/api/update/agnos_status?branch=${encodeURIComponent(targetBranch)}`) + const payload = await readJsonPayload(response) + if (!response.ok) { + throw new Error(payload.error || response.statusText || "Failed to check AGNOS update status") + } + + const agnosUpdate = payload.agnosUpdate || null + if (String(state.selectedBranch || "").trim() === targetBranch) { + state.selectedBranchAgnosUpdate = agnosUpdate + state.selectedBranchAgnosError = agnosUpdate?.error || "" + } + if (showToast && agnosUpdate?.available) { + showSnackbar("Selected branch includes an AGNOS firmware update.") + } + return agnosUpdate + } catch (error) { + const message = error?.message || "Failed to check AGNOS update status" + if (String(state.selectedBranch || "").trim() === targetBranch) { + state.selectedBranchAgnosUpdate = null + state.selectedBranchAgnosError = message + } + if (showToast) { + showSnackbar(message, "error") + } + return null + } finally { + if (state.selectedBranchAgnosTarget === targetBranch) { + state.selectedBranchAgnosBusy = false + } + } +} + function setAdvancedOptions(enabled) { const next = !!enabled state.showAdvancedOptions = next @@ -336,6 +451,7 @@ function setAdvancedOptions(enabled) { state.selectedBranch = currentBranch } state.hasManualBranchSelection = false + clearSelectedBranchAgnosStatus() } try { @@ -389,10 +505,13 @@ async function runFastUpdate(skipConfirmation = false) { } if (!skipConfirmation) { + const agnosWarning = agnosConfirmationText() const confirmed = window.confirm( "Fast update warning:\n\n" + "- This update method skips backup creation.\n" + - "- Your device will reboot when the update is done.\n\n" + + "- Your device will reboot when the update is done." + + agnosWarning + + "\n\n" + "Continue with fast update?" ) if (!confirmed) return @@ -452,10 +571,16 @@ async function runBranchSwitch(skipConfirmation = false) { const currentBranch = String(state.status?.branch || "").trim() const actionLabel = currentBranch && currentBranch === targetBranch ? "update" : "switch and update" if (!skipConfirmation) { + if (isSelectedBranchDifferent()) { + await fetchSelectedBranchAgnosStatus(false, true) + } + const agnosWarning = agnosConfirmationText() const confirmed = window.confirm( `This will ${actionLabel} to the '${targetBranch}' branch.\n\n` + "- This update method skips backup creation.\n" + - "- Your device will reboot when the update is done.\n\n" + + "- Your device will reboot when the update is done." + + agnosWarning + + "\n\n" + "Continue?" ) if (!confirmed) return @@ -686,6 +811,23 @@ export function UpdateManager() { ${() => state.status?.isOnroad ? html`

Onroad: actions disabled

` : ""} + ${() => isSelectedBranchDifferent() && state.selectedBranchAgnosBusy + ? html`

Checking AGNOS firmware impact for ${state.selectedBranch}...

` + : ""} + + ${() => activeAgnosUpdate()?.available ? html` +
+ AGNOS firmware update included +

This update changes the AGNOS firmware manifest.

+ + ${() => agnosChangedPartitionsText(activeAgnosUpdate()) + ? html`

${agnosChangedPartitionsText(activeAgnosUpdate())}

` + : ""} +
+ ` : ""} +