This commit is contained in:
firestar5683
2026-04-06 15:04:37 -05:00
parent d745f51b22
commit 033d83252e
3 changed files with 200 additions and 3 deletions
+28 -1
View File
@@ -101,12 +101,32 @@ class Soundd:
self.auto_volume = 0
self.previous_sound_pack = None
self.previous_sound_source_signature = None
self.error_log = ERROR_LOGS_PATH / "error.txt"
self.random_events_directory = RANDOM_EVENTS_PATH / "sounds"
self.update_starpilot_sounds()
def get_sound_source_signature(self):
try:
resolved_path = self.sound_directory.resolve()
except OSError:
resolved_path = self.sound_directory
sound_files = ()
try:
if self.sound_directory.exists():
sound_files = tuple(sorted(
(entry.name, entry.stat().st_mtime_ns)
for entry in self.sound_directory.iterdir()
if entry.is_file()
))
except OSError:
sound_files = ()
return str(resolved_path), sound_files
def load_sounds(self):
self.loaded_sounds: dict[int, np.ndarray] = {}
@@ -268,6 +288,8 @@ class Soundd:
self.starpilot_toggles = starpilot_toggles
stream = self.update_starpilot_sounds(sd, stream)
elif rk.frame % 5 == 0:
stream = self.update_starpilot_sounds(sd, stream)
def update_starpilot_sounds(self, sd=None, stream=None):
self.volume_map = {
@@ -295,10 +317,15 @@ class Soundd:
else:
self.sound_directory = Path(BASEDIR) / "selfdrive" / "assets" / "sounds"
if self.starpilot_toggles.sound_pack != self.previous_sound_pack:
sound_source_signature = self.get_sound_source_signature()
should_reload = self.starpilot_toggles.sound_pack != self.previous_sound_pack
should_reload |= sound_source_signature != self.previous_sound_source_signature
if should_reload:
self.load_sounds()
self.previous_sound_pack = self.starpilot_toggles.sound_pack
self.previous_sound_source_signature = sound_source_signature
if stream is not None:
stream.close()
@@ -42,6 +42,15 @@ const BUILTIN_THEME_OPTIONS = {
turn_signals: { name: "None (Stock)", path: "__stock_none__", type: "stock_none", builtin: true },
};
const PARAM_KEY_BY_ASSET_TYPE = {
colors: "ColorScheme",
distance_icons: "DistanceIconPack",
icons: "IconPack",
sounds: "SoundPack",
turn_signals: "SignalAnimation",
steering_wheel: "WheelIcon",
};
const fileStore = {
images: { distanceIcons: {} },
sounds: {},
@@ -107,6 +116,14 @@ const state = reactive({
themeToDelete: null,
themes: [],
activeTab: "colors",
selectedThemeSources: {
colors: null,
distance_icons: null,
icons: null,
sounds: null,
turn_signals: null,
steering_wheel: null,
},
fetched: false,
});
@@ -190,6 +207,7 @@ function handleDrop(e) {
};
state.sequentialImages = move(state.sequentialImages, draggedIndex, dropIndex);
fileStore.sequentialFiles = move(fileStore.sequentialFiles, draggedIndex, dropIndex);
state.selectedThemeSources.turn_signals = null;
}
draggedIndex = -1;
@@ -212,6 +230,20 @@ function handleDragEnd(e) {
const clearAsset = (type, key, subkey = null) => {
state.themeSubmitted = false;
const assetType = key === "turnSignal" || key === "turnSignalBlindspot"
? "turn_signals"
: key === "distanceIcons"
? "distance_icons"
: key === "homeButton" || key === "settingsButton"
? "icons"
: key === "steeringWheel"
? "steering_wheel"
: type === "audio"
? "sounds"
: null;
if (assetType) {
state.selectedThemeSources[assetType] = null;
}
if (key === "turnSignal") {
if (state.turnSignalType === "Sequential") {
fileStore.sequentialFiles = [];
@@ -243,6 +275,7 @@ const onClearClick = (e, type, key, subkey = null) => {
};
const clearAssetType = (assetType) => {
state.selectedThemeSources[assetType] = null;
if (assetType === "colors") {
state.colors = { ...defaultColors };
}
@@ -526,6 +559,20 @@ export function ThemeMaker() {
}
state.themeSubmitted = false;
const assetType = key === "turnSignal" || key === "turnSignalBlindspot"
? "turn_signals"
: key === "distanceIcons"
? "distance_icons"
: key === "homeButton" || key === "settingsButton"
? "icons"
: key === "steeringWheel"
? "steering_wheel"
: type === "audio"
? "sounds"
: null;
if (assetType) {
state.selectedThemeSources[assetType] = null;
}
if (key === "turnSignal" && state.turnSignalType === "Sequential") {
fileStore.sequentialFiles.push(...files);
@@ -555,6 +602,7 @@ export function ThemeMaker() {
alpha: 255,
};
state.themeSubmitted = false;
state.selectedThemeSources.colors = null;
};
const validateTurnSignalLength = (e) => {
@@ -562,21 +610,30 @@ export function ThemeMaker() {
const clampedValue = isNaN(value) ? 25 : Math.max(25, Math.min(1000, value));
state.turnSignalLength = clampedValue;
e.target.value = clampedValue;
state.selectedThemeSources.turn_signals = null;
};
const toggleTurnSignalType = (type) => {
state.turnSignalType = type;
state.themeSubmitted = false;
state.selectedThemeSources.turn_signals = null;
state.sequentialImages = [];
fileStore.sequentialFiles = [];
fileStore.images.turnSignal = undefined;
state.imageFileNames.turnSignal = "";
};
const setTurnSignalStyle = (style) => {
state.turnSignalStyle = style;
state.themeSubmitted = false;
state.selectedThemeSources.turn_signals = null;
};
const getFormData = () => {
const formData = new FormData();
formData.append("themeName", state.themeName);
formData.append("saveChecklist", JSON.stringify(state.saveChecklist));
formData.append("selectedThemeSources", JSON.stringify(state.selectedThemeSources));
if (state.discordUsername && state.discordUsername.trim()) {
formData.append("discordUsername", state.discordUsername.trim());
@@ -646,6 +703,48 @@ export function ThemeMaker() {
}
};
const persistThemeSelection = async (theme, assetType) => {
const paramKey = PARAM_KEY_BY_ASSET_TYPE[assetType];
if (!paramKey) {
return true;
}
let value = theme?.path || "";
if (theme?.type === "stock") {
value = "stock";
} else if (theme?.type === "stock_none") {
value = "none";
} else if (assetType === "steering_wheel" && value) {
value = value.replace(/\.[^.]+$/, "");
}
const customThemesResult = await performApiAction(
"/api/params",
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "CustomThemes", value: "true" }),
},
"",
"Failed to enable custom themes."
);
if (!customThemesResult.ok) {
return false;
}
const result = await performApiAction(
"/api/params",
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: paramKey, value }),
},
"",
`Failed to update ${assetType.replace("_", " ")}.`
);
return result.ok;
};
const applyTheme = async () => {
if (!Object.values(state.saveChecklist).some(v => v)) {
return showSnackbar("Please select at least one component to apply!", "error");
@@ -996,6 +1095,11 @@ export function ThemeMaker() {
}
}
state.selectedThemeSources[assetType] = {
path: theme.path,
type: theme.type,
};
showSnackbar(`Loaded ${assetType.replace("_", " ")} from "${theme.name}"!`);
} catch (err) {
console.error("Failed to load theme asset:", err);
@@ -1013,10 +1117,17 @@ export function ThemeMaker() {
state.turnSignalStyle = "Traditional";
state.imageFileNames.turnSignal = "None";
state.imageFileNames.turnSignalBlindspot = "";
state.selectedThemeSources.turn_signals = {
path: "__stock_none__",
type: "stock_none",
};
showSnackbar('Loaded stock turn signals.');
};
const loadSelectableThemeAsset = async (theme, assetType) => {
const persisted = await persistThemeSelection(theme, assetType);
if (!persisted) return;
if (theme?.type === "stock_none" && assetType === "turn_signals") {
loadBuiltinTurnSignals();
return;
@@ -1225,9 +1336,9 @@ export function ThemeMaker() {
</label>
<div class="signal-type-toggle">
<button class="${() => `toggle-button ${state.turnSignalStyle === "Static" ? "active" : ""}`}"
@click="${() => state.turnSignalStyle = "Static"}">Static</button>
@click="${() => setTurnSignalStyle("Static")}">Static</button>
<button class="${() => `toggle-button ${state.turnSignalStyle === "Traditional" ? "active" : ""}`}"
@click="${() => state.turnSignalStyle = "Traditional"}">Traditional</button>
@click="${() => setTurnSignalStyle("Traditional")}">Traditional</button>
</div>
${() => state.showTurnSignalHelp && html`<div class="turn-signal-help-text">
<p><strong>Static</strong> - The turn signal animation appears next to the current speed.</p>
+59
View File
@@ -4778,6 +4778,44 @@ def setup(app):
return jsonify({"error": error}), 400
save_checklist = json.loads(form_data.get("saveChecklist", "{}"))
selected_theme_sources = json.loads(form_data.get("selectedThemeSources", "{}"))
theme_param_map = {
"colors": "ColorScheme",
"distance_icons": "DistanceIconPack",
"icons": "IconPack",
"sounds": "SoundPack",
"turn_signals": "SignalAnimation",
"steering_wheel": "WheelIcon",
}
def get_selected_theme_value(asset_type):
source = selected_theme_sources.get(asset_type)
if not isinstance(source, dict):
return None
source_type = str(source.get("type") or "").strip().lower()
source_path = str(source.get("path") or "").strip()
if not source_path:
return None
if source_type == "stock":
return "stock"
if source_type == "stock_none":
return "none"
if source_path == "__stock__":
return "stock"
if source_path == "__stock_none__":
return "none"
if asset_type == "steering_wheel":
return Path(source_path).stem
return source_path
for asset_type, param_key in theme_param_map.items():
if save_checklist.get(asset_type):
selected_value = get_selected_theme_value(asset_type)
if selected_value is not None:
params.put(param_key, selected_value)
if save_checklist.get("colors") and temp_path is not None:
asset_location = temp_path / "colors"
@@ -4850,6 +4888,13 @@ def setup(app):
return stock_asset_path
stock_fallbacks = {
"icons/button_home.png": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "images" / "button_home.png",
"icons/button_settings.png": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "images" / "button_settings.png",
"sounds/disengage.wav": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "sounds" / "disengage.wav",
"sounds/engage.wav": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "sounds" / "engage.wav",
"sounds/prompt.wav": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "sounds" / "prompt.wav",
# Stock openpilot has no dedicated startup clip; runtime falls back to engage.
"sounds/startup.wav": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "sounds" / "engage.wav",
"steering_wheel/wheel.png": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "icons" / "chffr_wheel.png",
}
@@ -5116,6 +5161,13 @@ def setup(app):
"path": f"icons/{filename}"
}
break
elif theme_type == "stock" or theme_path == "__stock__":
for filename, response_key in [("button_home.png", "homeButton"), ("button_settings.png", "settingsButton")]:
if _resolve_stock_theme_asset_path(f"icons/{filename}").exists():
response_data["images"][response_key] = {
"filename": filename,
"path": f"icons/{filename}",
}
distance_dir = theme_dir / "distance_icons"
if distance_dir.exists():
@@ -5179,6 +5231,13 @@ def setup(app):
"filename": f"{name}.wav",
"path": f"sounds/{name}.wav"
}
elif theme_type == "stock" or theme_path == "__stock__":
for name in ["engage", "disengage", "startup", "prompt"]:
if _resolve_stock_theme_asset_path(f"sounds/{name}.wav").exists():
response_data["sounds"][name] = {
"filename": f"{name}.wav",
"path": f"sounds/{name}.wav"
}
steering_wheel_path = None
if theme_type == "stock" or theme_path == "__stock__":