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.
+
+ ${() => agnosWarningItems(activeAgnosUpdate()).map((warning) => html`- ${warning}
`)}
+
+ ${() => agnosChangedPartitionsText(activeAgnosUpdate())
+ ? html`
${agnosChangedPartitionsText(activeAgnosUpdate())}
`
+ : ""}
+
+ ` : ""}
+