diff --git a/scripts/laptop_device_build.sh b/scripts/laptop_device_build.sh index cafd178589..1f25ba54b2 100755 --- a/scripts/laptop_device_build.sh +++ b/scripts/laptop_device_build.sh @@ -467,66 +467,6 @@ EOF echo "==> larch64 scons completed in $(( $(date +%s) - started_at ))s" } -validate_larch64_artifacts() { - local engine - engine="$(detect_engine)" - - echo "==> Validating larch64 runtime dependencies" - "${engine}" run --rm --platform linux/arm64 \ - --user "${DOCKER_RUN_USER}" \ - -v "${HOST_ROOT_DIR}:/work" \ - -v "${HOST_VENV_DIR}:/work/.venv-linux-arm64:ro" \ - -v "${HOST_SYSROOT_DIR}:/opt/tici-sysroot:ro" \ - -v "${HOST_SYSROOT_DIR}/system/vendor/lib64:/system/vendor/lib64:ro" \ - -w /work \ - "${IMAGE_NAME}" bash -lc ' -set -euo pipefail -ffmpeg_lib_dir="/work/.venv-linux-arm64/lib/python3.12/site-packages/ffmpeg/install/lib" -target_ffmpeg_runpath="/usr/local/venv/lib/python3.12/site-packages/ffmpeg/install/lib" -runtime_library_path="${ffmpeg_lib_dir}:/work/third_party/acados/larch64/lib:/work/third_party/libyuv/larch64/lib:/work/selfdrive/controls/lib/lateral_mpc_lib/c_generated_code:/work/selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code:/opt/tici-sysroot/usr/local/lib:/opt/tici-sysroot/lib/aarch64-linux-gnu:/opt/tici-sysroot/usr/lib/aarch64-linux-gnu:/system/vendor/lib64" -failures=0 - -while IFS= read -r -d "" artifact; do - if ! readelf -h "${artifact}" 2>/dev/null | grep -q "Machine:.*AArch64"; then - continue - fi - - dynamic="$(readelf -d "${artifact}" 2>/dev/null || true)" - if grep -Eq "(RPATH|RUNPATH).*\/work\/" <<<"${dynamic}"; then - echo "ERROR: ${artifact} embeds a build-host runtime path" >&2 - failures=1 - fi - - has_ffmpeg=0 - while IFS= read -r needed; do - case "${needed}" in - libavformat.so.*|libavcodec.so.*|libswresample.so.*|libavutil.so.*) - has_ffmpeg=1 - if [[ ! -e "${ffmpeg_lib_dir}/${needed}" ]]; then - echo "ERROR: ${artifact} links ${needed}, which is not shipped by the managed FFmpeg package" >&2 - failures=1 - fi - ;; - esac - done < <(sed -n "s/.*Shared library: \[\([^]]*\)\].*/\1/p" <<<"${dynamic}") - - if [[ "${has_ffmpeg}" -eq 1 ]] && ! grep -Fq "${target_ffmpeg_runpath}" <<<"${dynamic}"; then - echo "ERROR: ${artifact} links FFmpeg without the target managed-venv RUNPATH" >&2 - failures=1 - fi - - missing="$(LD_LIBRARY_PATH="${runtime_library_path}" ldd "${artifact}" 2>&1 | grep "not found" || true)" - if [[ -n "${missing}" ]]; then - echo "ERROR: ${artifact} has unresolved target dependencies:" >&2 - echo "${missing}" >&2 - failures=1 - fi -done < <(git ls-files -z) - -exit "${failures}" -' -} - setup_host_venv() { if [[ ! -f "${ROOT_DIR}/.venv/bin/activate" ]]; then if [[ -x "${ROOT_DIR}/tools/install_python_dependencies.sh" ]]; then @@ -576,7 +516,6 @@ run_larch64_build() { cereal/messaging/bridge \ msgq_repo/msgq/ipc_pyx.so \ msgq_repo/msgq/visionipc/visionipc_pyx.so - validate_larch64_artifacts touch "${ROOT_DIR}/prebuilt" } diff --git a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py index 40d0c36582..811bffa5d9 100644 --- a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py +++ b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py @@ -359,7 +359,7 @@ GENESIS_G70_OUTPUT_SMOOTHING_UNWIND_RC = 0.10 GENESIS_G70_OUTPUT_SMOOTHING_UNWIND_PHASE = 0.04 GENESIS_G70_OUTPUT_SMOOTHING_UNWIND_PHASE_WIDTH = 0.08 GENESIS_G70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_LAT = 0.45 -GENESIS_G70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_RC = 0.055 +GENESIS_G70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_RC = 0.16 GENESIS_G70_ANGLE_OUTPUT_TAPER_MIN = 0.45 GENESIS_G70_ANGLE_OUTPUT_TAPER_START = 70.0 GENESIS_G70_ANGLE_OUTPUT_TAPER_WIDTH = 6.0 @@ -3483,7 +3483,7 @@ def get_genesis_g70_stabilized_output(output_torque: float, prev_output_torque: changing_direction = (abs(desired_lateral_accel) >= GENESIS_G70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_LAT and prev_output_torque * desired_lateral_accel <= 0.0) if changing_direction: - response_time = min(response_time, GENESIS_G70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_RC) + response_time = max(response_time, GENESIS_G70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_RC) output_alpha = dt / (max(response_time, 0.0) + dt) smoothed_output = prev_output_torque + output_alpha * (output_torque - prev_output_torque) diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index 764cdc8aeb..60a34a00d8 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -1834,7 +1834,7 @@ class TestLatControl: assert low_speed == pytest.approx(-0.2, abs=0.005) assert abs(high_speed_center - 0.2) < abs(low_speed - 0.2) assert high_speed_unwind > high_speed_wind > 0.1 - assert abs(high_speed_direction_change - 0.3) > abs(high_speed_center - 0.2) + assert 0.2 < high_speed_direction_change < 0.3 def test_genesis_g70_output_stabilizer_update_path(self, monkeypatch): calls = [] diff --git a/starpilot/system/the_galaxy/assets/mobile/js/api.js b/starpilot/system/the_galaxy/assets/mobile/js/api.js index 053eab8355..b1835cb6ef 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/api.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/api.js @@ -185,6 +185,25 @@ export const api = { getNavigation() { return request("/api/navigation") }, setNavigation(body) { return request("/api/navigation", { method: "POST", data: body }) }, + getNavigationFavorites() { return request("/api/navigation/favorite", { cache: "no-store" }) }, + mapboxSuggest(query, accessToken, sessionToken, context = {}) { + const params = new URLSearchParams({ access_token: accessToken, session_token: sessionToken, q: query, limit: "4", ...context }) + return request(`https://api.mapbox.com/search/searchbox/v1/suggest?${params.toString()}`, { cache: "no-store" }) + }, + mapboxRetrieve(mapboxId, accessToken, sessionToken) { + const params = new URLSearchParams({ access_token: accessToken, session_token: sessionToken }) + return request(`https://api.mapbox.com/search/searchbox/v1/retrieve/${encodeURIComponent(mapboxId)}?${params.toString()}`, { cache: "no-store" }) + }, + mapboxGeocode(query, accessToken, context = {}) { + const params = new URLSearchParams({ access_token: accessToken, q: query, ...context }) + return request(`https://api.mapbox.com/search/geocode/v6/forward?${params.toString()}`, { cache: "no-store" }) + }, + mapboxDirections(from, to, accessToken) { + const origin = `${from.longitude},${from.latitude}` + const destination = `${to.longitude},${to.latitude}` + const params = new URLSearchParams({ geometries: "geojson", annotations: "congestion", overview: "full", alternatives: "true", access_token: accessToken }) + return request(`https://api.mapbox.com/directions/v5/mapbox/driving-traffic/${origin};${destination}?${params.toString()}`, { cache: "no-store" }) + }, getNavigationKeys() { return request("/api/navigation_key") }, setNavigationKey(body) { return request("/api/navigation_key", { method: "POST", data: body }) }, navigationFavorite(body) { return request("/api/navigation/favorite", { method: "POST", data: body }) }, diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/NavigationDestinationPanel.js b/starpilot/system/the_galaxy/assets/mobile/js/components/NavigationDestinationPanel.js new file mode 100644 index 0000000000..229b1d5e90 --- /dev/null +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/NavigationDestinationPanel.js @@ -0,0 +1,318 @@ +import { api, showSnackbar } from "../api.js" +import { getMapboxSearchContext, addRouteToMap, removeRouteFromMap } from "../../../components/navigation/navigation_utilities.js?v=nav-search-context-2" + +const MAPBOX_STYLE = "mapbox://styles/frogsgomoo/cmcfv151j000o01rcdxebhl76" + +let mapboxLoadPromise = null + +function loadMapboxGL() { + if (mapboxLoadPromise) return mapboxLoadPromise + mapboxLoadPromise = new Promise((resolve, reject) => { + if (window.mapboxgl) return resolve(window.mapboxgl) + const link = document.createElement("link") + link.rel = "stylesheet" + link.href = "https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css" + document.head.appendChild(link) + const script = document.createElement("script") + script.src = "https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.js" + script.onload = () => resolve(window.mapboxgl) + script.onerror = () => reject(new Error("Failed to load Mapbox GL")) + document.head.appendChild(script) + }) + return mapboxLoadPromise +} + +function number(value) { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null +} + +function coordinates(value) { + if (Array.isArray(value) && value.length >= 2) { + const longitude = number(value[0]) + const latitude = number(value[1]) + return longitude === null || latitude === null ? null : { longitude, latitude } + } + const latitude = number(value?.latitude) + const longitude = number(value?.longitude) + return latitude === null || longitude === null ? null : { latitude, longitude } +} + +function parseJson(value, fallback) { + if (Array.isArray(value)) return value + if (typeof value !== "string" || !value.trim()) return fallback + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) ? parsed : fallback + } catch (e) { + return fallback + } +} + +function parseObject(value) { + if (value && typeof value === "object" && !Array.isArray(value)) return value + if (typeof value !== "string" || !value.trim()) return null + try { + const parsed = JSON.parse(value) + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null + } catch (e) { + return null + } +} + +function labelFor(place) { + return String(place?.full_address || place?.place_name || place?.name || place?.address || "").trim() +} + +function secondaryLabel(place) { + const primary = String(place?.name || place?.text || "").trim() + const full = labelFor(place) + return full && full.toLowerCase() !== primary.toLowerCase() ? full : "" +} + +export const NavigationDestinationPanel = { + name: "NavigationDestinationPanel", + data() { + return { + loading: true, + searching: false, + loadingRoute: false, + error: "", + query: "", + suggestions: [], + recentDestinations: [], + favorites: [], + destination: null, + mapboxPublic: "", + language: "", + lastPosition: null, + map: null, + mapReady: false, + currentMarker: null, + destinationMarker: null, + searchTimer: null, + searchRequest: 0, + sessionToken: globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2), + } + }, + computed: { + hasMapbox() { return !!this.mapboxPublic }, + recentPlaces() { + const seen = new Set() + return [...this.favorites, ...this.recentDestinations].filter((place) => { + const coords = coordinates(place) + const key = coords ? `${coords.latitude}:${coords.longitude}` : labelFor(place).toLowerCase() + if (!key || seen.has(key)) return false + seen.add(key) + return true + }).slice(0, 10) + }, + }, + async mounted() { + await this.load() + }, + beforeUnmount() { + clearTimeout(this.searchTimer) + if (this.map) { + removeRouteFromMap(this.map) + this.map.remove() + this.map = null + } + }, + methods: { + async load() { + try { + const [nav, favoritePayload] = await Promise.all([ + api.getNavigation(), + api.getNavigationFavorites().catch(() => ({ favorites: [] })), + ]) + this.mapboxPublic = String(nav?.mapboxPublic || "").trim() + this.language = String(nav?.language || "").trim() + this.lastPosition = coordinates(nav?.lastPosition) + this.favorites = Array.isArray(favoritePayload?.favorites) ? favoritePayload.favorites : [] + this.recentDestinations = parseJson(nav?.previousDestinations, []) + const saved = parseObject(nav?.destination) + const savedDestination = coordinates(nav?.destination) || coordinates(saved) + if (savedDestination) { + const raw = saved || nav?.destination || {} + this.destination = { ...raw, ...savedDestination, name: labelFor(raw) || "Current destination" } + this.query = this.destination.name + } + } catch (e) { + this.error = e?.message || "Failed to load navigation." + } finally { + this.loading = false + if (this.hasMapbox) { + await this.$nextTick() + await this.setupMap() + } + } + }, + async setupMap() { + if (!this.hasMapbox || this.map || !this.$refs.map) return + try { + const mapboxgl = await loadMapboxGL() + mapboxgl.accessToken = this.mapboxPublic + const center = this.lastPosition || coordinates(this.destination) || { longitude: 0, latitude: 0 } + this.map = new mapboxgl.Map({ + container: this.$refs.map, + center: [center.longitude, center.latitude], + zoom: this.lastPosition ? 15 : (this.destination ? 12 : 2), + style: MAPBOX_STYLE, + attributionControl: false, + logoPosition: "bottom-right", + }) + this.map.on("load", () => { + this.mapReady = true + if (this.lastPosition) this.currentMarker = new mapboxgl.Marker().setLngLat([this.lastPosition.longitude, this.lastPosition.latitude]).addTo(this.map) + if (this.destination) this.previewDestination(this.destination) + }) + } catch (e) { + this.error = e?.message || "Failed to load the map." + } + }, + searchContext(query) { + const context = getMapboxSearchContext(query, this.lastPosition, [this.language, ...(navigator.languages || [navigator.language])]) + if (this.lastPosition) context.proximity = `${this.lastPosition.longitude},${this.lastPosition.latitude}` + return context + }, + onInput(event) { + this.destination = null + this.searchRequest += 1 + this.searching = false + this.error = "" + clearTimeout(this.searchTimer) + const rawValue = event?.target?.value ?? this.query + this.query = String(rawValue) + const value = this.query.trim() + if (value.length < 3 || !this.hasMapbox) { + this.suggestions = [] + return + } + this.searchTimer = setTimeout(() => this.search(value), 350) + }, + async search(value) { + const request = ++this.searchRequest + this.searching = true + try { + const payload = await api.mapboxSuggest(value, this.mapboxPublic, this.sessionToken, this.searchContext(value)) + if (request === this.searchRequest) this.suggestions = Array.isArray(payload?.suggestions) ? payload.suggestions : [] + } catch (e) { + if (request === this.searchRequest) this.error = e?.message || "Destination search failed." + } finally { + if (request === this.searchRequest) this.searching = false + } + }, + async resolvePlace(place) { + const placeLabel = labelFor(place) || this.query.trim() + let coords = coordinates(place?.geometry?.coordinates) || coordinates(place) + if (!coords && place?.mapbox_id) { + const payload = await api.mapboxRetrieve(place.mapbox_id, this.mapboxPublic, this.sessionToken) + coords = coordinates(payload?.features?.[0]?.geometry?.coordinates) + } + if (!coords) { + const payload = await api.mapboxGeocode(placeLabel, this.mapboxPublic, this.searchContext(placeLabel)) + coords = coordinates(payload?.features?.[0]?.geometry?.coordinates) + } + if (!coords) throw new Error("Could not determine that location.") + return { ...coords, name: placeLabel, place_name: placeLabel } + }, + async chooseSuggestion(place) { + this.searching = true + try { + this.destination = await this.resolvePlace(place) + this.query = this.destination.name + this.suggestions = [] + await this.previewDestination(this.destination) + } catch (e) { + this.error = e?.message || "Could not determine that location." + showSnackbar(this.error, "error") + } finally { + this.searching = false + } + }, + async resolveQuery() { + const value = this.query.trim() + if (!value) return null + if (this.destination && this.destination.name === value) return this.destination + return this.resolvePlace({ name: value }) + }, + async setDestination(place = null) { + if (!this.hasMapbox) { + showSnackbar("Add a Mapbox public key in App Keys first.", "error") + return + } + this.loadingRoute = true + try { + this.destination = place || await this.resolveQuery() + if (!this.destination) throw new Error("Enter a destination first.") + await api.setNavigation(this.destination) + this.query = this.destination.name + this.suggestions = [] + await this.previewDestination(this.destination) + showSnackbar("Destination set.") + } catch (e) { + this.error = e?.message || "Failed to set destination." + showSnackbar(this.error, "error") + } finally { + this.loadingRoute = false + } + }, + async previewDestination(place) { + if (!this.mapReady || !this.map || !place) return + const mapboxgl = window.mapboxgl + this.destinationMarker?.remove() + this.destinationMarker = new mapboxgl.Marker({ color: "#9d72ff" }).setLngLat([place.longitude, place.latitude]).addTo(this.map) + if (!this.lastPosition) { + this.map.flyTo({ center: [place.longitude, place.latitude], zoom: 14 }) + return + } + try { + const payload = await api.mapboxDirections(this.lastPosition, place, this.mapboxPublic) + const routes = Array.isArray(payload?.routes) ? payload.routes : [] + if (routes.length) { + removeRouteFromMap(this.map) + addRouteToMap(this.map, routes, [this.lastPosition.longitude, this.lastPosition.latitude], [place.longitude, place.latitude], () => {}, true, () => "main") + } else { + this.map.fitBounds([[this.lastPosition.longitude, this.lastPosition.latitude], [place.longitude, place.latitude]], { padding: 80, duration: 500 }) + } + } catch (e) { + this.map.fitBounds([[this.lastPosition.longitude, this.lastPosition.latitude], [place.longitude, place.latitude]], { padding: 80, duration: 500 }) + } + }, + usePlace(place) { this.chooseSuggestion(place) }, + }, + template: ` +
Add a Mapbox public key in App Keys to search destinations and show the map.
+{{ error }}
+