mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-22 09:42:10 +08:00
oo buddy boy
This commit is contained in:
Binary file not shown.
+2
-1
@@ -2264,7 +2264,8 @@ struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 {
|
||||
|
||||
struct DriverMonitoringState {
|
||||
lockout @0 :Bool;
|
||||
lockoutRecoveryPercent @11 :Int8;
|
||||
lockoutCount @15 :Int8;
|
||||
lockoutMinutesRemaining @11 :Int8;
|
||||
alert3Count @12 :Int8;
|
||||
noResponseCount @13 :Int8;
|
||||
noResponseForceDecel @14 :Bool;
|
||||
|
||||
+61
-1
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CHUNK_SIZE = 45 * 1024 * 1024 # 45MB, under GitHub's 50MB limit
|
||||
@@ -13,9 +16,16 @@ def get_manifest_path(name):
|
||||
return f"{name}.chunkmanifest"
|
||||
|
||||
|
||||
def _chunk_paths(path, num_chunks):
|
||||
return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
|
||||
|
||||
|
||||
def get_chunk_paths(path, file_size):
|
||||
num_chunks = math.ceil(file_size / CHUNK_SIZE)
|
||||
return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
|
||||
return _chunk_paths(path, num_chunks)
|
||||
|
||||
|
||||
get_chunk_targets = get_chunk_paths
|
||||
|
||||
|
||||
def chunk_file(path, targets):
|
||||
@@ -31,6 +41,51 @@ def chunk_file(path, targets):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def get_existing_chunks(path):
|
||||
if os.path.isfile(path):
|
||||
return [path]
|
||||
manifest_path = get_manifest_path(path)
|
||||
if os.path.isfile(manifest_path):
|
||||
num_chunks = int(Path(manifest_path).read_text().strip())
|
||||
return _chunk_paths(path, num_chunks)
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
|
||||
def file_chunked_exists(path) -> bool:
|
||||
return os.path.isfile(path) or os.path.isfile(get_manifest_path(path))
|
||||
|
||||
|
||||
class ChunkStream(io.RawIOBase):
|
||||
def __init__(self, paths):
|
||||
self._paths = iter(paths)
|
||||
self._buffer = memoryview(b"")
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def readinto(self, buffer):
|
||||
count = 0
|
||||
while count < len(buffer):
|
||||
if not self._buffer:
|
||||
path = next(self._paths, None)
|
||||
if path is None:
|
||||
break
|
||||
self._buffer = memoryview(Path(path).read_bytes())
|
||||
continue
|
||||
take = min(len(buffer) - count, len(self._buffer))
|
||||
buffer[count:count + take] = self._buffer[:take]
|
||||
self._buffer = self._buffer[take:]
|
||||
count += take
|
||||
return count
|
||||
|
||||
|
||||
def open_file_chunked(path):
|
||||
chunks = get_existing_chunks(path)
|
||||
if chunks and chunks[0] == get_manifest_path(path):
|
||||
chunks = chunks[1:]
|
||||
return io.BufferedReader(ChunkStream(chunks))
|
||||
|
||||
|
||||
def read_file_chunked(path):
|
||||
manifest_path = get_manifest_path(path)
|
||||
if os.path.isfile(manifest_path):
|
||||
@@ -39,3 +94,8 @@ def read_file_chunked(path):
|
||||
if os.path.isfile(path):
|
||||
return Path(path).read_bytes()
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
file_path = sys.argv[1]
|
||||
chunk_file(file_path, get_chunk_targets(file_path, os.path.getsize(file_path)))
|
||||
|
||||
Binary file not shown.
@@ -37,6 +37,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}},
|
||||
{"EcuDisableFailed", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"AlphaLongitudinalEnabled", {PERSISTENT, BOOL}},
|
||||
{"ExperimentalLongitudinalEnabled", {PERSISTENT, BOOL}},
|
||||
@@ -148,6 +149,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UpdaterLastFetchTime", {PERSISTENT, TIME}},
|
||||
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// StarPilot variables
|
||||
@@ -457,6 +461,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"OneLaneChange", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"OnroadDistanceButton", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"OnroadDistanceButtonPressed", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
|
||||
{"FavoriteVirtualAccelCruiseCounter", {CLEAR_ON_MANAGER_START, INT, "0", "0"}},
|
||||
{"FavoriteVirtualDecelCruiseCounter", {CLEAR_ON_MANAGER_START, INT, "0", "0"}},
|
||||
{"WheelButtonBookmarkCounter", {CLEAR_ON_MANAGER_START, INT, "0", "0"}},
|
||||
{"openpilotMinutes", {PERSISTENT, INT, "0", "0", 0}},
|
||||
{"OverpassRequests", {PERSISTENT, JSON, "{}", "{}"}},
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
from openpilot.common import file_chunker
|
||||
|
||||
|
||||
def test_chunked_stream_round_trip(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(file_chunker, "CHUNK_SIZE", 7)
|
||||
path = tmp_path / "artifact.pkl"
|
||||
payload = b"a model artifact spanning several chunks"
|
||||
path.write_bytes(payload)
|
||||
|
||||
targets = file_chunker.get_chunk_targets(path, len(payload))
|
||||
file_chunker.chunk_file(path, targets)
|
||||
|
||||
assert file_chunker.file_chunked_exists(path)
|
||||
assert file_chunker.read_file_chunked(path) == payload
|
||||
with file_chunker.open_file_chunked(path) as stream:
|
||||
assert stream.read(9) + stream.read() == payload
|
||||
+20
-1
@@ -6,7 +6,7 @@ This workflow rebuilds StarPilot driving and driver-monitoring artifacts for the
|
||||
|
||||
- The supported build device is `comma@192.168.3.110`.
|
||||
- Never run these commands against `192.168.3.109`.
|
||||
- Do not compile normal and big-GPU artifacts together. This workflow builds normal QCOM artifacts only.
|
||||
- Normal artifacts target QCOM. External-GPU artifacts must be compiled explicitly and tagged in the manifest.
|
||||
- Keep source ONNX files and compiled PKLs on the T5 workspace, not the comma.
|
||||
|
||||
## Workspace
|
||||
@@ -82,6 +82,23 @@ The lower-level device compiler also supports direct use:
|
||||
./models --model deeprl3v2 --input-format supercombo --version v15
|
||||
```
|
||||
|
||||
For a model that cannot run on the device GPU, compile with the USB AMD GPU attached:
|
||||
|
||||
```bash
|
||||
./models --model heavyweight --input-format supercombo --version v15 --external-gpu
|
||||
```
|
||||
|
||||
This emits a streaming out-of-band pickle and keeps QCOM available for camera warps. Its manifest entry must include:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "heavyweight",
|
||||
"uses_external_gpu": true
|
||||
}
|
||||
```
|
||||
|
||||
Only tagged models activate the external GPU. If the GPU or artifact is unavailable, runtime falls back to the built-in model; all untagged models retain the existing QCOM path.
|
||||
|
||||
`--version` records behavioral semantics only. It does not change artifact layout.
|
||||
|
||||
If the compiled PKL exceeds 100 MiB, `./models` automatically keeps the full
|
||||
@@ -139,6 +156,8 @@ The generator preserves existing IDs and behavioral metadata and adds
|
||||
Repository-hosted multipart files are discovered by naming convention, so no
|
||||
size, hash, format, or part-count metadata is required.
|
||||
|
||||
`uses_external_gpu` is optional and defaults to `false`.
|
||||
|
||||
## Runtime Verification
|
||||
|
||||
Compilation validates JIT capture/replay, pickle round-trip, finite outputs, metadata slices, and both camera warps. Before release:
|
||||
|
||||
@@ -80,6 +80,7 @@ def parse_args() -> argparse.Namespace:
|
||||
)
|
||||
parser.add_argument("--list", action="store_true", help="List staged models and exit.")
|
||||
parser.add_argument("--force", action="store_true", help="Accepted for compatibility; selected outputs are always replaced.")
|
||||
parser.add_argument("--external-gpu", action="store_true", help="Compile the driving artifact for the USB AMD GPU.")
|
||||
parser.add_argument("--split-artifact", type=Path, help="Split an existing oversized PKL without compiling.")
|
||||
parser.add_argument("--chunk-size-mib", type=int, default=95, help="Multipart size in MiB; must be below 100.")
|
||||
parser.add_argument(
|
||||
@@ -355,6 +356,7 @@ def compile_driving(
|
||||
version: str,
|
||||
output_dir: Path,
|
||||
image_history_pipeline: str,
|
||||
external_gpu: bool = False,
|
||||
) -> Path:
|
||||
model_type, source_args = driving_compile_args(files, input_format)
|
||||
output_path = output_dir / f"{model_key}_driving_tinygrad.pkl"
|
||||
@@ -390,7 +392,20 @@ def compile_driving(
|
||||
]
|
||||
if version:
|
||||
command += ["--behavior-version", version]
|
||||
subprocess.run(command, cwd=REPO_ROOT, env=build_compile_env(), check=True)
|
||||
compile_env = build_compile_env()
|
||||
if external_gpu:
|
||||
for qcom_only_flag in ("IMAGE", "NOLOCALS", "OPENPILOT_HACKS"):
|
||||
compile_env.pop(qcom_only_flag, None)
|
||||
compile_env.update({
|
||||
"DEBUG": "2",
|
||||
"DEV": "USB+AMD:LLVM",
|
||||
"WARP_DEV": "QCOM",
|
||||
"FLOAT16": "1",
|
||||
"JIT_BATCH_SIZE": "0",
|
||||
"GMMU": "0",
|
||||
})
|
||||
command.append("--out-of-band")
|
||||
subprocess.run(command, cwd=REPO_ROOT, env=compile_env, check=True)
|
||||
return output_path
|
||||
|
||||
|
||||
@@ -487,7 +502,8 @@ def main() -> int:
|
||||
version = "v15"
|
||||
version_label = version or "unspecified behavior"
|
||||
print(f"Compiling {model_key} ({input_format}, {version_label}) from {args.input_dir} -> {args.output_dir}")
|
||||
output = compile_driving(model_key, files, input_format, version, args.output_dir, args.image_history_pipeline)
|
||||
output = compile_driving(model_key, files, input_format, version, args.output_dir,
|
||||
args.image_history_pipeline, args.external_gpu)
|
||||
print(f" saved {output.name}")
|
||||
multipart_outputs = split_oversized_artifact(output)
|
||||
if multipart_outputs:
|
||||
|
||||
@@ -236,11 +236,14 @@ def compile_model(model_id: str, source: dict, version: str, workspace: Path, fo
|
||||
run(["rsync", "-az", "--exclude=._*", f"{source_dir}/", f"{REMOTE}:{remote_input}/"])
|
||||
|
||||
log_path = workspace / "logs" / f"{model_id}.log"
|
||||
command = (
|
||||
f"cd {REMOTE_ROOT} && ./models --model {model_id} "
|
||||
f"--input-dir {remote_input} --output-dir {REMOTE_ROOT}/compiledmodels "
|
||||
f"--input-format {source['input_format']} --version {version}"
|
||||
)
|
||||
command_parts = [
|
||||
f"cd {REMOTE_ROOT} && ./models --model {model_id}",
|
||||
f"--input-dir {remote_input} --output-dir {REMOTE_ROOT}/compiledmodels",
|
||||
f"--input-format {source['input_format']} --version {version}",
|
||||
]
|
||||
if source.get("uses_external_gpu"):
|
||||
command_parts.append("--external-gpu")
|
||||
command = " ".join(command_parts)
|
||||
with open(log_path, "wb") as log_file:
|
||||
process = subprocess.run(["ssh", REMOTE, command], stdout=log_file, stderr=subprocess.STDOUT)
|
||||
if process.returncode != 0:
|
||||
@@ -288,7 +291,7 @@ def validate_model(model_id: str, version: str, workspace: Path) -> dict:
|
||||
return payload
|
||||
|
||||
|
||||
def update_manifest(base_manifest: Path, workspace: Path) -> dict:
|
||||
def update_manifest(base_manifest: Path, workspace: Path, source_map: dict) -> dict:
|
||||
payload = load_json(base_manifest)
|
||||
models = payload["models"] if isinstance(payload, dict) else payload
|
||||
if not any(model.get("id") == "deeprl3v2" for model in models):
|
||||
@@ -302,6 +305,9 @@ def update_manifest(base_manifest: Path, workspace: Path) -> dict:
|
||||
})
|
||||
multipart_handoff = []
|
||||
for model in models:
|
||||
source = source_map.get(model["id"], {})
|
||||
if "uses_external_gpu" in source:
|
||||
model["uses_external_gpu"] = bool(source["uses_external_gpu"])
|
||||
artifact = workspace / "compiled" / f"{model['id']}_driving_tinygrad.pkl"
|
||||
model.pop("artifact_format", None)
|
||||
model.pop("artifact_size", None)
|
||||
@@ -350,7 +356,7 @@ def main() -> int:
|
||||
if args.command == "manifest":
|
||||
if args.base_manifest is None:
|
||||
parser.error("--base-manifest is required")
|
||||
update_manifest(args.base_manifest, args.workspace)
|
||||
update_manifest(args.base_manifest, args.workspace, source_map)
|
||||
return 0
|
||||
|
||||
model_ids = [args.model] if args.model else list(source_map)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import statistics
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Benchmark fixed-shape ONNX forwards through OpenCV DNN.")
|
||||
parser.add_argument("models", nargs="+", type=Path, help="ONNX model paths to benchmark.")
|
||||
parser.add_argument("--input-size", type=int, required=True, help="Square model input size.")
|
||||
parser.add_argument("--iterations", type=int, default=50)
|
||||
parser.add_argument("--warmup", type=int, default=5)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def percentile(values: list[float], quantile: float) -> float:
|
||||
ordered = sorted(values)
|
||||
index = min(round((len(ordered) - 1) * quantile), len(ordered) - 1)
|
||||
return ordered[index]
|
||||
|
||||
|
||||
def benchmark(path: Path, input_size: int, iterations: int, warmup: int) -> dict[str, object]:
|
||||
net = cv2.dnn.readNetFromONNX(str(path))
|
||||
blob = np.zeros((1, 3, input_size, input_size), dtype=np.float32)
|
||||
net.setInput(blob)
|
||||
output = net.forward()
|
||||
for _ in range(max(warmup - 1, 0)):
|
||||
net.setInput(blob)
|
||||
output = net.forward()
|
||||
|
||||
durations_ms = []
|
||||
for _ in range(max(iterations, 1)):
|
||||
net.setInput(blob)
|
||||
started_at = time.perf_counter()
|
||||
output = net.forward()
|
||||
durations_ms.append((time.perf_counter() - started_at) * 1000.0)
|
||||
|
||||
return {
|
||||
"model": str(path.resolve()),
|
||||
"input_size": input_size,
|
||||
"output_shape": list(output.shape),
|
||||
"iterations": len(durations_ms),
|
||||
"median_ms": round(statistics.median(durations_ms), 3),
|
||||
"p95_ms": round(percentile(durations_ms, 0.95), 3),
|
||||
"mean_ms": round(statistics.mean(durations_ms), 3),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
results = [
|
||||
benchmark(path.expanduser().resolve(), args.input_size, args.iterations, args.warmup)
|
||||
for path in args.models
|
||||
]
|
||||
print(json.dumps(results, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -86,6 +86,13 @@ def parse_read(text: str) -> tuple[str, str]:
|
||||
return (speed, confidence) if separator else ("", "")
|
||||
|
||||
|
||||
def nonzero_value(text: str) -> str:
|
||||
try:
|
||||
return text if float(text) > 0.0 else ""
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def queue_row(row: dict[str, str]) -> dict[str, str]:
|
||||
key = record_key(row)
|
||||
route, dongle_id, log_id = route_identity(row)
|
||||
@@ -99,8 +106,17 @@ def queue_row(row: dict[str, str]) -> dict[str, str]:
|
||||
read_sources += ";logged_vision_publish"
|
||||
|
||||
review_reasons = ["corrected_source_timing"]
|
||||
review_priority = float(row.get("score") or 0.0)
|
||||
if row.get("event_type", "") == "visionPublish":
|
||||
review_reasons.extend(("route_vision_publish", f"published_{row.get('published_speed', '')}"))
|
||||
published_speed = row.get("published_speed", "")
|
||||
review_reasons.extend(("route_vision_publish", f"published_{published_speed}"))
|
||||
if published_speed and candidate_speed and published_speed != candidate_speed:
|
||||
review_reasons.append("logged_current_value_disagreement")
|
||||
review_priority += 5.0
|
||||
map_speed = nonzero_value(row.get("map_speed", ""))
|
||||
if map_speed and published_speed and map_speed != published_speed:
|
||||
review_reasons.append("logged_map_disagreement")
|
||||
review_priority += 3.0
|
||||
else:
|
||||
review_reasons.append("route_bookmark")
|
||||
|
||||
@@ -129,9 +145,9 @@ def queue_row(row: dict[str, str]) -> dict[str, str]:
|
||||
"read_sources": read_sources,
|
||||
"read_support_count": "1",
|
||||
"is_regulatory": row.get("is_regulatory", ""),
|
||||
"map_current_speed_limit_mph": row.get("map_speed", ""),
|
||||
"map_next_speed_limit_mph": row.get("next_speed", ""),
|
||||
"review_priority": row.get("score", ""),
|
||||
"map_current_speed_limit_mph": nonzero_value(row.get("map_speed", "")),
|
||||
"map_next_speed_limit_mph": nonzero_value(row.get("next_speed", "")),
|
||||
"review_priority": f"{review_priority:.4f}",
|
||||
"review_reasons": ";".join(review_reasons),
|
||||
})
|
||||
return item
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import EXTENDED_CLASSIFIER_SPEED_VALUES # type: ignore # noqa: TID251
|
||||
else:
|
||||
from .common import EXTENDED_CLASSIFIER_SPEED_VALUES
|
||||
|
||||
|
||||
DEFAULT_CLASSES = tuple(str(value) for value in EXTENDED_CLASSIFIER_SPEED_VALUES) + ("reject",)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Expand a YOLO classification head while preserving existing class rows.")
|
||||
parser.add_argument("--model", type=Path, required=True, help="Existing Ultralytics classification checkpoint.")
|
||||
parser.add_argument("--output", type=Path, required=True, help="Expanded checkpoint path.")
|
||||
parser.add_argument("--classes", nargs="+", default=DEFAULT_CLASSES, help="New output classes in dataset index order.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from ultralytics import YOLO
|
||||
|
||||
args = parse_args()
|
||||
model = YOLO(str(args.model.expanduser().resolve()))
|
||||
old_names = {int(index): str(name) for index, name in model.names.items()}
|
||||
new_names = dict(enumerate(args.classes))
|
||||
if len(set(new_names.values())) != len(new_names):
|
||||
raise ValueError("--classes contains duplicate names")
|
||||
if not set(old_names.values()).issubset(new_names.values()):
|
||||
missing = sorted(set(old_names.values()) - set(new_names.values()))
|
||||
raise ValueError(f"Expanded classes omit existing outputs: {missing}")
|
||||
|
||||
head = model.model.model[-1]
|
||||
old_linear = head.linear
|
||||
new_linear = torch.nn.Linear(
|
||||
old_linear.in_features,
|
||||
len(new_names),
|
||||
bias=old_linear.bias is not None,
|
||||
device=old_linear.weight.device,
|
||||
dtype=old_linear.weight.dtype,
|
||||
)
|
||||
new_index_by_name = {name: index for index, name in new_names.items()}
|
||||
with torch.no_grad():
|
||||
for old_index, name in old_names.items():
|
||||
new_index = new_index_by_name[name]
|
||||
new_linear.weight[new_index].copy_(old_linear.weight[old_index])
|
||||
if old_linear.bias is not None:
|
||||
new_linear.bias[new_index].copy_(old_linear.bias[old_index])
|
||||
|
||||
head.linear = new_linear
|
||||
head.np = sum(parameter.numel() for parameter in head.parameters())
|
||||
model.model.names = new_names
|
||||
model.model.yaml["nc"] = len(new_names)
|
||||
model.model.args["classes"] = None
|
||||
|
||||
output = args.output.expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
model.save(str(output))
|
||||
print(f"Expanded {len(old_names)} outputs to {len(new_names)}: {output}")
|
||||
print(new_names)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -222,7 +222,11 @@ def save_classifier_crop(base_dir: Path, split: str, speed_value: int, image_bgr
|
||||
def collect_backgrounds(background_dir: Path) -> list[Path]:
|
||||
if not background_dir.is_dir():
|
||||
return []
|
||||
return sorted(path for path in background_dir.iterdir() if path.suffix.lower() in {".jpg", ".jpeg", ".png"})
|
||||
return sorted(
|
||||
path
|
||||
for path in background_dir.iterdir()
|
||||
if not path.name.startswith("._") and path.suffix.lower() in {".jpg", ".jpeg", ".png"}
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -233,6 +237,7 @@ def main():
|
||||
parser.add_argument("--val-count", type=int, default=1200, help="Number of synthetic validation detector images.")
|
||||
parser.add_argument("--negative-ratio", type=float, default=0.18, help="Share of detector images with no sign.")
|
||||
parser.add_argument("--speed-values", nargs="+", type=int, default=list(DEFAULT_SPEED_VALUES), help="Posted values to synthesize.")
|
||||
parser.add_argument("--regulatory-only", action="store_true", help="Generate only regulatory signs for the requested values.")
|
||||
parser.add_argument("--seed", type=int, default=20260330, help="Random seed.")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -265,7 +270,11 @@ def main():
|
||||
detector_lines: list[str] = []
|
||||
|
||||
if rng.random() >= args.negative_ratio:
|
||||
sign_spec = choose_sign_spec(rng, speed_values)
|
||||
sign_spec = (
|
||||
SignSpec(detector_class=0, style="regulatory", speed_value=rng.choice(speed_values))
|
||||
if args.regulatory_only
|
||||
else choose_sign_spec(rng, speed_values)
|
||||
)
|
||||
if sign_spec.style == "advisory":
|
||||
sign_image = render_advisory_sign(sign_spec.speed_value or 25, seed=rng.randint(0, 1_000_000))
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
if __package__ in (None, ""):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from common import EXTENDED_CLASSIFIER_SPEED_VALUES # type: ignore # noqa: TID251
|
||||
else:
|
||||
from .common import EXTENDED_CLASSIFIER_SPEED_VALUES
|
||||
|
||||
|
||||
IMAGE_SUFFIXES = frozenset((".jpg", ".jpeg", ".png", ".bmp", ".webp"))
|
||||
SUPPORTED_CLASSES = frozenset(str(value) for value in EXTENDED_CLASSIFIER_SPEED_VALUES) | {"reject"}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Merge image-folder classifier datasets using hard links when possible.")
|
||||
parser.add_argument("sources", nargs="+", type=Path, help="Classifier roots containing train/ and val/.")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--overwrite", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def link_or_copy(source: Path, destination: Path) -> None:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
os.link(source, destination)
|
||||
except OSError:
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
sources = [path.expanduser().resolve() for path in args.sources]
|
||||
output = args.output.expanduser().resolve()
|
||||
if output.exists():
|
||||
if not args.overwrite:
|
||||
raise FileExistsError(f"Output already exists: {output}")
|
||||
shutil.rmtree(output)
|
||||
|
||||
counts: Counter[str] = Counter()
|
||||
for source_index, source_root in enumerate(sources):
|
||||
for split in ("train", "val"):
|
||||
split_root = source_root / split
|
||||
if not split_root.is_dir():
|
||||
raise FileNotFoundError(split_root)
|
||||
for class_dir in sorted(split_root.iterdir()):
|
||||
if not class_dir.is_dir() or class_dir.name.startswith("._"):
|
||||
continue
|
||||
if class_dir.name not in SUPPORTED_CLASSES:
|
||||
raise ValueError(f"Unsupported classifier class {class_dir.name!r} in {source_root}")
|
||||
for image_path in sorted(class_dir.iterdir()):
|
||||
if image_path.name.startswith("._") or image_path.suffix.lower() not in IMAGE_SUFFIXES or not image_path.is_file():
|
||||
continue
|
||||
destination = output / split / class_dir.name / f"s{source_index:02d}_{image_path.name}"
|
||||
link_or_copy(image_path, destination)
|
||||
counts[f"{split}/{class_dir.name}"] += 1
|
||||
|
||||
summary = {
|
||||
"sources": [str(path) for path in sources],
|
||||
"output": str(output),
|
||||
"counts": dict(sorted(counts.items())),
|
||||
}
|
||||
(output / "merge_summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -21,7 +21,7 @@ VIDEO_SUFFIXES = {".hevc", ".mp4", ".mov", ".mkv", ".avi"}
|
||||
|
||||
def iter_source_files(paths: list[Path]):
|
||||
for input_path in paths:
|
||||
if input_path.is_file():
|
||||
if input_path.is_file() and not input_path.name.startswith("._"):
|
||||
yield input_path
|
||||
continue
|
||||
|
||||
@@ -29,7 +29,7 @@ def iter_source_files(paths: list[Path]):
|
||||
continue
|
||||
|
||||
for path in sorted(input_path.rglob("*")):
|
||||
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES | VIDEO_SUFFIXES:
|
||||
if path.is_file() and not path.name.startswith("._") and path.suffix.lower() in IMAGE_SUFFIXES | VIDEO_SUFFIXES:
|
||||
yield path
|
||||
|
||||
|
||||
|
||||
@@ -321,19 +321,32 @@ function setSpeed(speed, shouldSave) {
|
||||
|
||||
function handleDigitShortcut(digit) {
|
||||
speedBuffer += digit;
|
||||
if (speedBuffer.length > 2) speedBuffer = speedBuffer.slice(-2);
|
||||
if (speedBuffer.length > 3) speedBuffer = digit;
|
||||
if (speedBufferTimer) clearTimeout(speedBufferTimer);
|
||||
speedBufferTimer = setTimeout(clearSpeedBuffer, 1000);
|
||||
|
||||
if (speedBuffer.length < 2) return;
|
||||
|
||||
if (speedBuffer === "10") {
|
||||
clearTimeout(speedBufferTimer);
|
||||
speedBufferTimer = setTimeout(() => {
|
||||
if (speedBuffer === "10") {
|
||||
clearSpeedBuffer();
|
||||
setSpeed(10, true);
|
||||
}
|
||||
}, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
const speed = Number(speedBuffer);
|
||||
clearSpeedBuffer();
|
||||
if (speeds.includes(speed)) {
|
||||
clearSpeedBuffer();
|
||||
setSpeed(speed, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (speedBuffer.length < 3) return;
|
||||
|
||||
speedBuffer = digit;
|
||||
speedBufferTimer = setTimeout(clearSpeedBuffer, 1000);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ from openpilot.selfdrive.car.cruise import (
|
||||
from openpilot.selfdrive.car.redneck_cruise import RedneckCruise, select_redneck_target_speed
|
||||
from openpilot.selfdrive.car.car_specific import MockCarState
|
||||
|
||||
from openpilot.starpilot.common.favorite_slots import (
|
||||
FAVORITE_ACTION_ACCEL_COUNTER,
|
||||
FAVORITE_ACTION_DECEL_COUNTER,
|
||||
)
|
||||
from openpilot.starpilot.common.starpilot_variables import get_starpilot_toggles, update_starpilot_toggles
|
||||
from openpilot.starpilot.controls.starpilot_card import StarPilotCard
|
||||
|
||||
@@ -93,6 +97,9 @@ class Car:
|
||||
|
||||
self.params = Params()
|
||||
self.params_memory = Params(memory=True)
|
||||
self._favorite_virtual_accel_counter = self.params_memory.get_int(FAVORITE_ACTION_ACCEL_COUNTER)
|
||||
self._favorite_virtual_decel_counter = self.params_memory.get_int(FAVORITE_ACTION_DECEL_COUNTER)
|
||||
self._favorite_virtual_releases = []
|
||||
|
||||
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
|
||||
|
||||
@@ -205,6 +212,27 @@ class Car:
|
||||
self.sm = self.sm.extend(['starpilotOnroadEvents', 'starpilotPlan', 'starpilotSelfdriveState', 'liveCalibration', 'selfdriveState'])
|
||||
self.pm = self.pm.extend(['starpilotCarState'])
|
||||
|
||||
def _inject_favorite_virtual_cruise_events(self, CS: car.CarState) -> None:
|
||||
virtual_events = [
|
||||
structs.CarState.ButtonEvent(pressed=False, type=button_type)
|
||||
for button_type in self._favorite_virtual_releases
|
||||
]
|
||||
self._favorite_virtual_releases = []
|
||||
|
||||
for counter_key, counter_attr, button_type in (
|
||||
(FAVORITE_ACTION_ACCEL_COUNTER, "_favorite_virtual_accel_counter", ButtonType.accelCruise),
|
||||
(FAVORITE_ACTION_DECEL_COUNTER, "_favorite_virtual_decel_counter", ButtonType.decelCruise),
|
||||
):
|
||||
counter = self.params_memory.get_int(counter_key)
|
||||
if counter == getattr(self, counter_attr):
|
||||
continue
|
||||
setattr(self, counter_attr, counter)
|
||||
virtual_events.append(structs.CarState.ButtonEvent(pressed=True, type=button_type))
|
||||
self._favorite_virtual_releases.append(button_type)
|
||||
|
||||
if virtual_events:
|
||||
CS.buttonEvents = list(CS.buttonEvents) + virtual_events
|
||||
|
||||
def state_update(self) -> tuple[car.CarState, structs.RadarDataT | None]:
|
||||
"""carState update loop, driven by can"""
|
||||
|
||||
@@ -215,6 +243,7 @@ class Car:
|
||||
CS, FPCS = self.CI.update(can_list, self.starpilot_toggles)
|
||||
if self.CP.brand == 'mock':
|
||||
CS, FPCS = self.mock_carstate.update(CS, FPCS)
|
||||
self._inject_favorite_virtual_cruise_events(CS)
|
||||
|
||||
# Update radar tracks from CAN
|
||||
RD: structs.RadarDataT | None = self.RI.update(can_list)
|
||||
|
||||
@@ -43,6 +43,8 @@ from tinygrad.device import Device
|
||||
from tinygrad.engine.jit import TinyJit
|
||||
from tinygrad.helpers import Context
|
||||
from tinygrad.tensor import Tensor
|
||||
from openpilot.selfdrive.modeld.helpers import dump_oob
|
||||
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
||||
|
||||
|
||||
ARTIFACT_FORMAT_VERSION = 1
|
||||
@@ -59,6 +61,7 @@ WARP_INPUTS = LEGACY_WARP_INPUTS
|
||||
SPLIT_POLICY_INPUTS = BASE_POLICY_INPUTS
|
||||
SUPERCOMBO_POLICY_INPUTS = BASE_POLICY_INPUTS
|
||||
WARP_DEV = os.getenv("WARP_DEV")
|
||||
OOB_PICKLE = False
|
||||
|
||||
|
||||
def _detect_desire_key(input_shapes):
|
||||
@@ -451,7 +454,14 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||
print("capture + replay")
|
||||
test_values, test_buffers = random_inputs_run(jit, seed)
|
||||
print("pickle round trip")
|
||||
jit = pickle.loads(pickle.dumps(jit))
|
||||
if OOB_PICKLE:
|
||||
with tempfile.TemporaryFile(dir=".") as artifact_file:
|
||||
dump_oob(jit, artifact_file)
|
||||
artifact_file.seek(0)
|
||||
from openpilot.selfdrive.modeld.helpers import load_oob
|
||||
jit = load_oob(artifact_file)
|
||||
else:
|
||||
jit = pickle.loads(pickle.dumps(jit))
|
||||
random_inputs_run(jit, seed, test_values, test_buffers, expect_match=True)
|
||||
random_inputs_run(jit, seed + 1, test_values, test_buffers, expect_match=False)
|
||||
return jit
|
||||
@@ -486,6 +496,7 @@ def validate_metadata(metadata):
|
||||
|
||||
|
||||
def main():
|
||||
global OOB_PICKLE
|
||||
from tinygrad.nn.onnx import OnnxRunner
|
||||
|
||||
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
|
||||
@@ -498,6 +509,7 @@ def main():
|
||||
parser.add_argument("--frame-skip", type=int)
|
||||
parser.add_argument("--behavior-version")
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--out-of-band", action="store_true", help="Stream model weights outside pickle opcodes for large artifacts.")
|
||||
parser.add_argument("--vision-onnx")
|
||||
parser.add_argument("--policy-onnx")
|
||||
parser.add_argument("--off-policy-onnx")
|
||||
@@ -510,6 +522,9 @@ def main():
|
||||
help="Where img/big_img history queues are updated. 'policy' is the newer faster ABI; 'warp' reproduces legacy v22 artifacts.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
OOB_PICKLE = args.out_of_band
|
||||
if "USB+AMD" in os.environ.get("DEV", ""):
|
||||
wait_usbgpu_link()
|
||||
|
||||
output = {
|
||||
"format_version": ARTIFACT_FORMAT_VERSION,
|
||||
@@ -619,7 +634,10 @@ def main():
|
||||
)
|
||||
|
||||
with open(args.output, "wb") as artifact_file:
|
||||
pickle.dump(output, artifact_file)
|
||||
if args.out_of_band:
|
||||
dump_oob(output, artifact_file)
|
||||
else:
|
||||
pickle.dump(output, artifact_file)
|
||||
print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from tinygrad.device import Device
|
||||
@@ -13,7 +18,8 @@ USBGPU_PID = 0x0001
|
||||
def _default_tinygrad_backend() -> str:
|
||||
env_dev = os.getenv("DEV", "").strip()
|
||||
if env_dev:
|
||||
return env_dev.split(":", 1)[0].split("+", 1)[-1] or env_dev
|
||||
default_target = env_dev.split(";", 1)[0]
|
||||
return default_target.split(":", 1)[0].split("+", 1)[-1] or default_target
|
||||
|
||||
try:
|
||||
default = Device.DEFAULT
|
||||
@@ -57,7 +63,7 @@ def get_tg_input_devices(process_name: str, usbgpu: bool) -> dict[str, str]:
|
||||
return _fallback_tg_devices(process_name, usbgpu)
|
||||
|
||||
|
||||
def modeld_pkl_path(usbgpu: bool):
|
||||
def modeld_pkl_path(usbgpu: bool) -> Path:
|
||||
prefix = "big_" if usbgpu else ""
|
||||
return MODELS_DIR / f"{prefix}driving_tinygrad.pkl"
|
||||
|
||||
@@ -71,3 +77,48 @@ def usbgpu_present() -> bool:
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def tinygrad_dev_config(usbgpu: bool, tici: bool) -> str:
|
||||
default = "QCOM" if tici else ("CPU:LLVM" if usbgpu else "LLVM")
|
||||
return f"{default};USB+AMD:LLVM" if usbgpu else default
|
||||
|
||||
|
||||
def dump_oob(obj, output) -> None:
|
||||
"""Pickle large buffers out-of-band so model weights are never duplicated in RAM."""
|
||||
with tempfile.TemporaryFile(dir=".") as buffers:
|
||||
def buffer_callback(pickle_buffer: pickle.PickleBuffer):
|
||||
view = pickle_buffer.raw()
|
||||
buffers.write(struct.pack("<q", view.nbytes))
|
||||
buffers.write(view)
|
||||
pickle_buffer.release()
|
||||
|
||||
opcodes = io.BytesIO()
|
||||
pickle.Pickler(opcodes, protocol=5, buffer_callback=buffer_callback).dump(obj)
|
||||
opcode_data = opcodes.getvalue()
|
||||
output.write(struct.pack("<q", len(opcode_data)))
|
||||
output.write(opcode_data)
|
||||
buffers.seek(0)
|
||||
shutil.copyfileobj(buffers, output)
|
||||
|
||||
|
||||
def load_oob(source):
|
||||
header = source.read(8)
|
||||
if len(header) != 8:
|
||||
raise EOFError("truncated out-of-band pickle header")
|
||||
opcodes = source.read(struct.unpack("<q", header)[0])
|
||||
|
||||
def buffers():
|
||||
previous = None
|
||||
while header := source.read(8):
|
||||
if len(header) != 8:
|
||||
raise EOFError("truncated out-of-band buffer header")
|
||||
if previous is not None:
|
||||
previous.release()
|
||||
buffer = bytearray(struct.unpack("<q", header)[0])
|
||||
if source.readinto(buffer) != len(buffer):
|
||||
raise EOFError("truncated out-of-band buffer")
|
||||
previous = pickle.PickleBuffer(buffer)
|
||||
yield previous
|
||||
|
||||
return pickle.load(io.BytesIO(opcodes), buffers=buffers())
|
||||
|
||||
+64
-12
@@ -16,7 +16,7 @@ from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
from openpilot.common.file_chunker import file_chunked_exists, open_file_chunked, read_file_chunked
|
||||
from openpilot.common.realtime import config_realtime_process, DT_MDL
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
@@ -38,8 +38,9 @@ from openpilot.selfdrive.modeld.compile_modeld import (
|
||||
make_split_input_queues,
|
||||
make_supercombo_input_queues,
|
||||
)
|
||||
from openpilot.selfdrive.modeld.helpers import get_tg_input_devices
|
||||
from openpilot.starpilot.assets.model_manager import ModelManager
|
||||
from openpilot.selfdrive.modeld.helpers import get_tg_input_devices, load_oob, tinygrad_dev_config, usbgpu_present
|
||||
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
||||
from openpilot.starpilot.assets.model_manager import ModelManager, model_uses_external_gpu
|
||||
from openpilot.starpilot.common.model_versions import is_tinygrad_model_version
|
||||
from openpilot.starpilot.common.starpilot_variables import get_starpilot_toggles, MODELS_PATH, params_memory
|
||||
|
||||
@@ -171,6 +172,21 @@ class FrameMeta:
|
||||
if vipc is not None:
|
||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||
|
||||
|
||||
def _load_model_artifact(path: Path):
|
||||
"""Load legacy pickle artifacts and the streaming OOB format used by large GPU models."""
|
||||
with open_file_chunked(path) as artifact_file:
|
||||
pickle_header = artifact_file.peek(2)[:2]
|
||||
legacy_pickle = (
|
||||
len(pickle_header) == 2 and pickle_header[0] == 0x80 and pickle_header[1] <= pickle.HIGHEST_PROTOCOL
|
||||
)
|
||||
if legacy_pickle:
|
||||
return pickle.load(artifact_file)
|
||||
|
||||
with open_file_chunked(path) as artifact_file:
|
||||
return load_oob(artifact_file)
|
||||
|
||||
|
||||
class ModelState:
|
||||
prev_desire: np.ndarray
|
||||
|
||||
@@ -190,9 +206,13 @@ class ModelState:
|
||||
)
|
||||
return numpy_inputs, prev_desired_curv_key
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int):
|
||||
def __init__(self, cam_w: int, cam_h: int, external_gpu_active: bool = False):
|
||||
params = Params()
|
||||
model_id = _canonical_model_id(_resolve_mirrored_param(params, "Model", "DrivingModel") or BUILTIN_MODEL_KEY)
|
||||
requires_external_gpu = model_uses_external_gpu(model_id)
|
||||
if requires_external_gpu and not external_gpu_active:
|
||||
cloudlog.error(f"Model {model_id} requires an external GPU; falling back to {BUILTIN_MODEL_KEY}")
|
||||
model_id = BUILTIN_MODEL_KEY
|
||||
use_builtin = model_id == BUILTIN_MODEL_KEY
|
||||
loaded_builtin = use_builtin
|
||||
if use_builtin:
|
||||
@@ -200,22 +220,25 @@ class ModelState:
|
||||
else:
|
||||
model_path = MODELS_PATH / f"{model_id}_driving_tinygrad.pkl"
|
||||
|
||||
if not model_path.is_file() and not use_builtin:
|
||||
if not file_chunked_exists(model_path) and not use_builtin:
|
||||
cloudlog.error(f"Missing model artifact {model_path}, downloading {model_id}...")
|
||||
try:
|
||||
ModelManager(params, params_memory).download_model(model_id)
|
||||
except Exception:
|
||||
cloudlog.exception(f"Failed to download model {model_id}")
|
||||
if not model_path.is_file() and not use_builtin:
|
||||
if not file_chunked_exists(model_path) and not use_builtin:
|
||||
fallback_path = Path(__file__).parent / "models" / "driving_tinygrad.pkl"
|
||||
if fallback_path.is_file():
|
||||
if file_chunked_exists(fallback_path):
|
||||
cloudlog.error(f"Falling back to builtin model artifact after {model_id} download failed")
|
||||
model_path = fallback_path
|
||||
loaded_builtin = True
|
||||
if not model_path.is_file():
|
||||
requires_external_gpu = False
|
||||
if not file_chunked_exists(model_path):
|
||||
raise FileNotFoundError(model_path)
|
||||
|
||||
artifact = pickle.loads(read_file_chunked(str(model_path)))
|
||||
self.uses_external_gpu = external_gpu_active and requires_external_gpu and not loaded_builtin
|
||||
artifact = (_load_model_artifact(model_path) if self.uses_external_gpu
|
||||
else pickle.loads(read_file_chunked(str(model_path))))
|
||||
if artifact.get("format_version") != ARTIFACT_FORMAT_VERSION:
|
||||
raise ValueError(
|
||||
f"Unsupported model artifact format {artifact.get('format_version')!r}; "
|
||||
@@ -294,7 +317,7 @@ class ModelState:
|
||||
@property
|
||||
def QUEUE_DEV(self) -> str:
|
||||
if not hasattr(self, "_queue_dev"):
|
||||
devices = get_tg_input_devices(PROCESS_NAME, usbgpu=False)
|
||||
devices = get_tg_input_devices(PROCESS_NAME, usbgpu=self.uses_external_gpu)
|
||||
self._warp_dev = devices["WARP_DEV"]
|
||||
self._queue_dev = devices["QUEUE_DEV"]
|
||||
return self._queue_dev
|
||||
@@ -415,6 +438,22 @@ def main(demo=False):
|
||||
setproctitle(PROCESS_NAME)
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
params = Params()
|
||||
selected_model = _canonical_model_id(_resolve_mirrored_param(params, "Model", "DrivingModel") or BUILTIN_MODEL_KEY)
|
||||
usbgpu_present_now = usbgpu_present()
|
||||
external_model_selected = model_uses_external_gpu(selected_model)
|
||||
external_artifact = MODELS_PATH / f"{selected_model}_driving_tinygrad.pkl"
|
||||
external_artifact_ready = external_model_selected and file_chunked_exists(external_artifact)
|
||||
external_gpu_requested = usbgpu_present_now and external_model_selected
|
||||
params.put_bool("UsbGpuPresent", usbgpu_present_now)
|
||||
params.put_bool("UsbGpuCompiled", external_artifact_ready)
|
||||
params.put_bool("UsbGpuActive", False)
|
||||
if external_gpu_requested:
|
||||
from tinygrad.helpers import DEV
|
||||
device_config = tinygrad_dev_config(True, TICI)
|
||||
DEV.value = device_config
|
||||
os.environ["DEV"] = device_config
|
||||
|
||||
# visionipc clients
|
||||
while True:
|
||||
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
||||
@@ -440,7 +479,21 @@ def main(demo=False):
|
||||
|
||||
start_time = time.monotonic()
|
||||
cloudlog.warning("loading model")
|
||||
model = ModelState(vipc_client_main.width, vipc_client_main.height)
|
||||
if external_gpu_requested:
|
||||
wait_usbgpu_link()
|
||||
try:
|
||||
model = ModelState(vipc_client_main.width, vipc_client_main.height, external_gpu_requested)
|
||||
except Exception:
|
||||
if not external_gpu_requested:
|
||||
raise
|
||||
cloudlog.exception(f"Failed to load external-GPU model {selected_model}; falling back to {BUILTIN_MODEL_KEY}")
|
||||
device_config = tinygrad_dev_config(False, TICI)
|
||||
DEV.value = device_config
|
||||
os.environ["DEV"] = device_config
|
||||
model = ModelState(vipc_client_main.width, vipc_client_main.height, False)
|
||||
external_gpu_active = model.uses_external_gpu
|
||||
params.put_bool("UsbGpuCompiled", external_model_selected and file_chunked_exists(external_artifact))
|
||||
params.put_bool("UsbGpuActive", external_gpu_active)
|
||||
cloudlog.warning(f"model loaded in {time.monotonic() - start_time:.1f}s, modeld starting")
|
||||
|
||||
# messaging
|
||||
@@ -448,7 +501,6 @@ def main(demo=False):
|
||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay", "starpilotPlan"])
|
||||
|
||||
publish_state = PublishState()
|
||||
params = Params()
|
||||
# setup filter to track dropped frames
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ)
|
||||
frame_id = 0
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob, tinygrad_dev_config
|
||||
|
||||
|
||||
def test_external_gpu_keeps_the_native_device_available():
|
||||
assert tinygrad_dev_config(True, tici=True) == "QCOM;USB+AMD:LLVM"
|
||||
assert tinygrad_dev_config(False, tici=True) == "QCOM"
|
||||
assert tinygrad_dev_config(True, tici=False) == "CPU:LLVM;USB+AMD:LLVM"
|
||||
|
||||
|
||||
def test_out_of_band_artifact_round_trip():
|
||||
artifact = {"weights": np.arange(32, dtype=np.float32), "metadata": {"version": 1}}
|
||||
stream = io.BytesIO()
|
||||
dump_oob(artifact, stream)
|
||||
stream.seek(0)
|
||||
|
||||
restored = load_oob(stream)
|
||||
assert restored["metadata"] == artifact["metadata"]
|
||||
np.testing.assert_array_equal(restored["weights"], artifact["weights"])
|
||||
@@ -0,0 +1,35 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.usb import CHESTNUT_PRODUCT_ID, CHESTNUT_VENDOR_ID, controller, read_int, usb_devices
|
||||
|
||||
STABLE_SECONDS = 2.0
|
||||
STABLE_THRESHOLD = 5.0
|
||||
|
||||
|
||||
def _chestnut_portli() -> Path | None:
|
||||
for device in usb_devices():
|
||||
if read_int(device / "idVendor", 16) != CHESTNUT_VENDOR_ID or \
|
||||
read_int(device / "idProduct", 16) != CHESTNUT_PRODUCT_ID:
|
||||
continue
|
||||
usb_controller = controller(device)
|
||||
if usb_controller is not None and (usb_controller / "portli").exists():
|
||||
return usb_controller / "portli"
|
||||
return None
|
||||
|
||||
|
||||
def wait_usbgpu_link(timeout: float = 30.0) -> None:
|
||||
portli = _chestnut_portli()
|
||||
if portli is None:
|
||||
return
|
||||
|
||||
start_time = time.monotonic()
|
||||
while time.monotonic() - start_time < timeout:
|
||||
start_errors = read_int(portli, 0)
|
||||
time.sleep(STABLE_SECONDS)
|
||||
error_rate = (read_int(portli, 0) - start_errors) / STABLE_SECONDS
|
||||
if error_rate <= STABLE_THRESHOLD:
|
||||
return
|
||||
cloudlog.warning(f"usbgpu link not stable: {error_rate:.0f} errors/s")
|
||||
cloudlog.error("usbgpu link never stabilized")
|
||||
@@ -61,7 +61,7 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
# lockout specs
|
||||
self._MAX_ALERT_3 = 2
|
||||
self._MAX_NO_RESPONSE = 1
|
||||
self._LOCKOUT_TIME = int(1800 / DT_DMON)
|
||||
self._LOCKOUT_TIMES = [int(60 * n_min / DT_DMON) for n_min in [1, 5, 15, 30]]
|
||||
|
||||
self._TIMEOUT_RECOVERY_FACTOR_MAX = 5.
|
||||
self._TIMEOUT_RECOVERY_FACTOR_MIN = 1.25
|
||||
@@ -173,7 +173,10 @@ class DriverMonitoring:
|
||||
self.cnt_since_alert_3 = 0
|
||||
self.no_response_timeout = int(self.settings._NO_RESPONSE_TIMEOUT / DT_DMON)
|
||||
self.no_response_cnt = 0
|
||||
self.lockout_time = 0
|
||||
self.lockout_active = Params().get_bool("DriverTooDistracted")
|
||||
self.lockout_count = Params().get("DriverLockoutCount") or 0
|
||||
self.lockout_duration = self.settings._LOCKOUT_TIMES[min(max(self.lockout_count - 1, 0), len(self.settings._LOCKOUT_TIMES) - 1)]
|
||||
self.lockout_time_elapsed = 0
|
||||
self.step_change = 0.
|
||||
self.active_policy = MonitoringPolicy.vision
|
||||
self.driver_interacting = False
|
||||
@@ -184,8 +187,6 @@ class DriverMonitoring:
|
||||
self.threshold_alert_2 = 0.
|
||||
self.dcam_uncertain_cnt = 0
|
||||
self.dcam_reset_cnt = 0
|
||||
self.too_distracted = Params().get_bool("DriverTooDistracted")
|
||||
|
||||
self._reset_awareness()
|
||||
self._set_policy(MonitoringPolicy.vision)
|
||||
|
||||
@@ -334,16 +335,20 @@ class DriverMonitoring:
|
||||
self.driver_interacting = driver_engaged
|
||||
|
||||
if self.alert_3_cnt >= self.settings._MAX_ALERT_3 or self.no_response_cnt >= self.settings._MAX_NO_RESPONSE:
|
||||
self.too_distracted = True
|
||||
if not self.lockout_active:
|
||||
self.lockout_count += 1
|
||||
self.lockout_duration = self.settings._LOCKOUT_TIMES[min(self.lockout_count - 1, len(self.settings._LOCKOUT_TIMES) - 1)]
|
||||
Params().put("DriverLockoutCount", self.lockout_count)
|
||||
self.lockout_active = True
|
||||
|
||||
if self.too_distracted:
|
||||
self.lockout_time += 1
|
||||
if self.lockout_time > self.settings._LOCKOUT_TIME:
|
||||
self.too_distracted = False
|
||||
if self.lockout_active:
|
||||
self.lockout_time_elapsed += 1
|
||||
if self.lockout_time_elapsed > self.lockout_duration:
|
||||
self.lockout_active = False
|
||||
self.alert_3_cnt = 0
|
||||
self.cnt_since_alert_3 = 0
|
||||
self.no_response_cnt = 0
|
||||
self.lockout_time = 0
|
||||
self.lockout_time_elapsed = 0
|
||||
|
||||
always_on_valid = self.always_on and not wrong_gear
|
||||
if (self.driver_interacting and self.awareness > 0 and self.active_policy == MonitoringPolicy.wheeltouch) or \
|
||||
@@ -403,8 +408,10 @@ class DriverMonitoring:
|
||||
dat = messaging.new_message('driverMonitoringState', valid=valid)
|
||||
dm = dat.driverMonitoringState
|
||||
|
||||
dm.lockout = self.too_distracted
|
||||
dm.lockoutRecoveryPercent = to_percent(self.lockout_time / self.settings._LOCKOUT_TIME)
|
||||
dm.lockout = self.lockout_active
|
||||
dm.lockoutCount = self.lockout_count
|
||||
if self.lockout_active:
|
||||
dm.lockoutMinutesRemaining = max(1, round((self.lockout_duration - self.lockout_time_elapsed) * DT_DMON / 60.))
|
||||
dm.alert3Count = self.alert_3_cnt
|
||||
dm.noResponseCount = self.no_response_cnt
|
||||
dm.noResponseForceDecel = self.alert_level == AlertLevel.three and self.cnt_since_alert_3 >= self.no_response_timeout
|
||||
|
||||
@@ -113,21 +113,17 @@ class TestMonitoring:
|
||||
# engaged, distracted past red and beyond the no-response window -> unavailability response + lockout
|
||||
def test_distracted_lockout(self):
|
||||
alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
|
||||
s = d_status.settings
|
||||
assert alert_lvls[int(DISTRACTED_SECONDS_TO_RED / DT_DMON)] == 3
|
||||
assert d_status.alert_3_cnt == 1
|
||||
assert d_status.no_response_cnt == s._MAX_NO_RESPONSE
|
||||
assert d_status.too_distracted
|
||||
assert d_status.lockout_time > 0
|
||||
assert d_status.lockout_active
|
||||
assert d_status.lockout_time_elapsed > 0
|
||||
assert d_status.lockout_count >= 1
|
||||
|
||||
# no face -> wheeltouch red, sustained past the no-response timeout -> unavailability response + lockout
|
||||
def test_invisible_lockout(self):
|
||||
_, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
|
||||
s = d_status.settings
|
||||
assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.wheeltouch
|
||||
assert d_status.alert_3_cnt == 1
|
||||
assert d_status.no_response_cnt == s._MAX_NO_RESPONSE
|
||||
assert d_status.too_distracted
|
||||
assert d_status.lockout_active
|
||||
assert d_status.lockout_count >= 1
|
||||
|
||||
# engaged, no face detected the whole time, no action
|
||||
def test_fully_invisible_driver(self):
|
||||
|
||||
@@ -11,10 +11,9 @@ import cereal.messaging as messaging
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.git import get_short_branch
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_CTRL, DT_DMON
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import LaneChangeDirection
|
||||
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER
|
||||
from openpilot.selfdrive.monitoring.policy import DRIVER_MONITOR_SETTINGS
|
||||
from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER
|
||||
from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
@@ -29,9 +28,6 @@ StarPilotAlertStatus = custom.StarPilotSelfdriveState.AlertStatus
|
||||
StarPilotAudibleAlert = custom.StarPilotCarControl.HUDControl.AudibleAlert
|
||||
StarPilotEventName = custom.StarPilotOnroadEvent.EventName
|
||||
|
||||
DMON_LOCKOUT_TIME = DRIVER_MONITOR_SETTINGS()._LOCKOUT_TIME
|
||||
|
||||
|
||||
# Alert priorities
|
||||
class Priority(IntEnum):
|
||||
LOWEST = 0
|
||||
@@ -288,7 +284,7 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
|
||||
|
||||
def too_distracted_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality, starpilot_toggles: SimpleNamespace) -> Alert:
|
||||
if sm['driverMonitoringState'].lockout:
|
||||
mins_left = max(1, round((100 - sm['driverMonitoringState'].lockoutRecoveryPercent) / 100 * DMON_LOCKOUT_TIME * DT_DMON / 60.))
|
||||
mins_left = sm['driverMonitoringState'].lockoutMinutesRemaining
|
||||
return NoEntryAlert("Too Distracted", f"{mins_left} minute{'s' if mins_left != 1 else ''} Left", priority=Priority.HIGH)
|
||||
return NoEntryAlert("Pay Attention to Engage", priority=Priority.HIGH)
|
||||
|
||||
|
||||
@@ -96,12 +96,18 @@ class MiciHomeLayout(Widget):
|
||||
self._current_model_name = "default"
|
||||
|
||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||
self._egpu_label = UnifiedLabel("eGPU", font_size=30, text_color=rl.Color(0, 255, 96, 255),
|
||||
font_weight=FontWeight.SEMI_BOLD, max_width=76, wrap_text=False)
|
||||
self._egpu_label_gray = UnifiedLabel("eGPU", font_size=30, text_color=rl.GRAY,
|
||||
font_weight=FontWeight.SEMI_BOLD, max_width=76, wrap_text=False)
|
||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||
|
||||
self._status_bar_layout = HBoxLayout([
|
||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||
NetworkIcon(),
|
||||
self._experimental_icon,
|
||||
self._egpu_label,
|
||||
self._egpu_label_gray,
|
||||
self._mic_icon,
|
||||
], spacing=18)
|
||||
|
||||
@@ -211,6 +217,8 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
# ***** Center-aligned bottom section icons *****
|
||||
self._experimental_icon.set_visible(self._experimental_mode)
|
||||
self._egpu_label.set_visible(ui_state.usbgpu_active)
|
||||
self._egpu_label_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_active)
|
||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||
|
||||
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
|
||||
|
||||
@@ -21,7 +21,7 @@ from openpilot.selfdrive.ui.mici.onroad.starpilot_status import (
|
||||
)
|
||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.lib.starpilot_visuals import get_border_width
|
||||
from openpilot.starpilot.common.favorite_slots import load_favorite_slots, toggle_favorite_slot
|
||||
from openpilot.starpilot.common.favorite_slots import is_favorite_action_key, load_favorite_slots, toggle_favorite_slot
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
@@ -262,7 +262,7 @@ class FavoriteSlotsOverlay(Widget):
|
||||
rl.draw_rectangle_rounded_lines_ex(panel_rect, 0.16, 12, 3, accent)
|
||||
|
||||
label = slot.get("label") or slot.get("key") or "Favorite"
|
||||
state_text = "ON" if self._feedback_value else "OFF"
|
||||
state_text = "PRESS" if is_favorite_action_key(slot.get("key")) else ("ON" if self._feedback_value else "OFF")
|
||||
lines, font_size = self._fit_label(label, panel_rect.width - 20, panel_rect.height - 46)
|
||||
line_height = font_size * 1.08
|
||||
label_height = len(lines) * line_height
|
||||
@@ -310,7 +310,7 @@ class FavoriteSlotsOverlay(Widget):
|
||||
self._feedback_started_at = rl.get_time()
|
||||
slot = dict(self._visible_slots()).get(self._pressed_slot, {})
|
||||
key = slot.get("key")
|
||||
self._feedback_value = not ui_state.params.get_bool(key) if key else None
|
||||
self._feedback_value = None if is_favorite_action_key(key) else (not ui_state.params.get_bool(key) if key else None)
|
||||
self._interacting = True
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent):
|
||||
|
||||
@@ -81,6 +81,9 @@ class UIState:
|
||||
self.is_metric: bool = self.params.get_bool("IsMetric")
|
||||
self.is_release = self.params.get_bool("IsReleaseBranch")
|
||||
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
|
||||
self.usbgpu: bool = self.params.get_bool("UsbGpuPresent")
|
||||
self.usbgpu_compiled: bool = self.params.get_bool("UsbGpuCompiled")
|
||||
self.usbgpu_active: bool = self.params.get_bool("UsbGpuActive")
|
||||
self.started: bool = False
|
||||
self.ignition: bool = False
|
||||
self.recording_audio: bool = False
|
||||
@@ -182,6 +185,9 @@ class UIState:
|
||||
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.always_on_dm = self.params.get_bool("AlwaysOnDM")
|
||||
self.usbgpu = self.params.get_bool("UsbGpuPresent")
|
||||
self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled")
|
||||
self.usbgpu_active = self.params.get_bool("UsbGpuActive")
|
||||
self.switchback_mode_enabled = self.params_memory.get_bool("SwitchbackModeEnabled") if self.started else False
|
||||
if self.sm.valid.get("starpilotCarState", False):
|
||||
starpilot_car_state = self.sm["starpilotCarState"]
|
||||
|
||||
@@ -68,6 +68,19 @@ def model_key_aliases(model_key: str) -> list[str]:
|
||||
return [alias for alias in dict.fromkeys(alias for alias in aliases if alias)]
|
||||
|
||||
|
||||
def load_model_artifact_metadata(model_key: str) -> dict:
|
||||
try:
|
||||
payload = json.loads((MODELS_PATH / ARTIFACT_METADATA_CACHE).read_text())
|
||||
metadata = payload.get(canonical_model_key(model_key), {}) if isinstance(payload, dict) else {}
|
||||
return metadata if isinstance(metadata, dict) else {}
|
||||
except (OSError, ValueError, TypeError):
|
||||
return {}
|
||||
|
||||
|
||||
def model_uses_external_gpu(model_key: str) -> bool:
|
||||
return bool(load_model_artifact_metadata(model_key).get("uses_external_gpu", False))
|
||||
|
||||
|
||||
class ModelManager:
|
||||
def __init__(self, params, params_memory, boot_run=False):
|
||||
self.params = params
|
||||
@@ -291,6 +304,7 @@ class ModelManager:
|
||||
"artifact_size": int(model.get("artifact_size") or 0),
|
||||
"artifact_sha256": str(model.get("artifact_sha256") or "").strip().lower(),
|
||||
"artifact_url": str(model.get("artifact_url") or model.get("download_url") or "").strip(),
|
||||
"uses_external_gpu": bool(model.get("uses_external_gpu", False)),
|
||||
}
|
||||
return metadata
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from scripts import model_compiler
|
||||
from scripts.model_compiler import split_oversized_artifact
|
||||
from openpilot.starpilot.assets import download_functions
|
||||
from openpilot.starpilot.assets import model_manager
|
||||
from openpilot.starpilot.assets.model_manager import MANIFEST_CANDIDATES, ModelManager
|
||||
from openpilot.starpilot.common.model_versions import UNIFIED_ARTIFACT_FORMAT
|
||||
|
||||
@@ -21,6 +24,42 @@ def test_behavior_version_does_not_control_artifact_layout():
|
||||
assert manager._required_files("example", "split") == []
|
||||
|
||||
|
||||
def test_external_gpu_requirement_is_cached_from_manifest(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(model_manager, "MODELS_PATH", tmp_path)
|
||||
manager = object.__new__(ModelManager)
|
||||
metadata = manager._build_artifact_metadata_map([
|
||||
{"id": "large", "artifact_format": UNIFIED_ARTIFACT_FORMAT, "uses_external_gpu": True},
|
||||
{"id": "normal", "artifact_format": UNIFIED_ARTIFACT_FORMAT},
|
||||
])
|
||||
(tmp_path / model_manager.ARTIFACT_METADATA_CACHE).write_text(json.dumps(metadata))
|
||||
|
||||
assert model_manager.model_uses_external_gpu("large")
|
||||
assert not model_manager.model_uses_external_gpu("normal")
|
||||
assert not model_manager.model_uses_external_gpu("missing")
|
||||
|
||||
|
||||
def test_external_gpu_compilation_is_opt_in(tmp_path, monkeypatch):
|
||||
invocations = []
|
||||
monkeypatch.setattr(model_compiler, "build_compile_env", lambda: {
|
||||
"DEV": "QCOM", "IMAGE": "2", "NOLOCALS": "1", "OPENPILOT_HACKS": "1",
|
||||
})
|
||||
monkeypatch.setattr(model_compiler.subprocess, "run", lambda command, **kwargs: invocations.append((command, kwargs)))
|
||||
files = {"driving_supercombo": tmp_path / "model.onnx"}
|
||||
|
||||
model_compiler.compile_driving("normal", files, "supercombo", "v15", tmp_path, "policy")
|
||||
model_compiler.compile_driving("large", files, "supercombo", "v15", tmp_path, "policy", external_gpu=True)
|
||||
|
||||
normal_command, normal_kwargs = invocations[0]
|
||||
external_command, external_kwargs = invocations[1]
|
||||
assert "--out-of-band" not in normal_command
|
||||
assert normal_kwargs["env"]["DEV"] == "QCOM"
|
||||
assert normal_kwargs["env"]["IMAGE"] == "2"
|
||||
assert "--out-of-band" in external_command
|
||||
assert external_kwargs["env"]["DEV"] == "USB+AMD:LLVM"
|
||||
assert external_kwargs["env"]["WARP_DEV"] == "QCOM"
|
||||
assert all(flag not in external_kwargs["env"] for flag in ("IMAGE", "NOLOCALS", "OPENPILOT_HACKS"))
|
||||
|
||||
|
||||
def test_dropbox_urls_are_direct_downloads():
|
||||
url = "https://www.dropbox.com/scl/fi/id/model.pkl?rlkey=key&st=value&dl=0"
|
||||
normalized = download_functions.normalize_download_url(url)
|
||||
|
||||
@@ -10,6 +10,29 @@ from openpilot.common.params import ParamKeyType, Params
|
||||
|
||||
FAVORITE_SLOTS_PARAM = "StarPilotFavoriteSlots"
|
||||
FAVORITE_SLOT_COUNT = 3
|
||||
FAVORITE_ACTION_PREFIX = "__starpilot_favorite_action__:"
|
||||
FAVORITE_ACTION_DISTANCE_DECREASE = f"{FAVORITE_ACTION_PREFIX}distance_decrease"
|
||||
FAVORITE_ACTION_DISTANCE_INCREASE = f"{FAVORITE_ACTION_PREFIX}distance_increase"
|
||||
FAVORITE_ACTION_DECEL_COUNTER = "FavoriteVirtualDecelCruiseCounter"
|
||||
FAVORITE_ACTION_ACCEL_COUNTER = "FavoriteVirtualAccelCruiseCounter"
|
||||
FAVORITE_ACTION_OPTIONS = (
|
||||
{
|
||||
"key": FAVORITE_ACTION_DISTANCE_DECREASE,
|
||||
"label": "Distance - / SET",
|
||||
"description": "Acts like a short press of the car's SET/- cruise button.",
|
||||
"section": "Actions",
|
||||
"action": "decelCruise",
|
||||
},
|
||||
{
|
||||
"key": FAVORITE_ACTION_DISTANCE_INCREASE,
|
||||
"label": "Distance + / RES",
|
||||
"description": "Acts like a short press of the car's RES/+ cruise button.",
|
||||
"section": "Actions",
|
||||
"action": "accelCruise",
|
||||
},
|
||||
)
|
||||
FAVORITE_ACTION_KEYS = {option["key"] for option in FAVORITE_ACTION_OPTIONS}
|
||||
FAVORITE_ACTION_LABELS = {option["key"]: option["label"] for option in FAVORITE_ACTION_OPTIONS}
|
||||
|
||||
|
||||
def default_favorite_slots() -> list[dict[str, Any]]:
|
||||
@@ -50,6 +73,23 @@ def _get_key_type(params: Params, key: str):
|
||||
return None
|
||||
|
||||
|
||||
def is_favorite_action_key(key: str | None) -> bool:
|
||||
return bool(key) and key in FAVORITE_ACTION_KEYS
|
||||
|
||||
|
||||
def favorite_key_is_valid(params: Params, key: str | None, eligible_keys: Iterable[str] | None = None) -> bool:
|
||||
if not key:
|
||||
return False
|
||||
|
||||
if is_favorite_action_key(key):
|
||||
return True
|
||||
|
||||
if eligible_keys is not None and key not in set(eligible_keys):
|
||||
return False
|
||||
|
||||
return _get_key_type(params, key) == ParamKeyType.BOOL
|
||||
|
||||
|
||||
def is_bool_param(params: Params, key: str | None, eligible_keys: Iterable[str] | None = None) -> bool:
|
||||
if not key:
|
||||
return False
|
||||
@@ -73,10 +113,15 @@ def normalize_favorite_slots(raw_slots: Any, params: Params | None = None,
|
||||
if key is not None:
|
||||
key = str(key).strip() or None
|
||||
|
||||
if key and ((eligible is not None and key not in eligible) or (params is not None and not is_bool_param(params, key))):
|
||||
if key and is_favorite_action_key(key):
|
||||
pass
|
||||
elif key and (
|
||||
(eligible is not None and key not in eligible) or
|
||||
(params is not None and not is_bool_param(params, key))
|
||||
):
|
||||
key = None
|
||||
|
||||
label = str(raw_slot.get("label") or "").strip()
|
||||
label = str(raw_slot.get("label") or FAVORITE_ACTION_LABELS.get(key, "")).strip()
|
||||
if len(label) > 32:
|
||||
label = label[:32].rstrip()
|
||||
|
||||
@@ -111,6 +156,20 @@ def request_starpilot_toggle_refresh(params_memory: Params | None = None) -> Non
|
||||
params_memory.put_bool("StarPilotTogglesUpdated", True)
|
||||
|
||||
|
||||
def trigger_favorite_action(key: str | None, params_memory: Params | None = None) -> bool:
|
||||
if not is_favorite_action_key(key):
|
||||
return False
|
||||
|
||||
params_memory = params_memory or Params(memory=True)
|
||||
counter_key = (
|
||||
FAVORITE_ACTION_ACCEL_COUNTER
|
||||
if key == FAVORITE_ACTION_DISTANCE_INCREASE
|
||||
else FAVORITE_ACTION_DECEL_COUNTER
|
||||
)
|
||||
params_memory.put_int(counter_key, params_memory.get_int(counter_key) + 1)
|
||||
return True
|
||||
|
||||
|
||||
def toggle_favorite_slot(slot_index: int, params: Params | None = None, params_memory: Params | None = None) -> bool:
|
||||
if slot_index < 0 or slot_index >= FAVORITE_SLOT_COUNT:
|
||||
return False
|
||||
@@ -119,7 +178,13 @@ def toggle_favorite_slot(slot_index: int, params: Params | None = None, params_m
|
||||
slots = load_favorite_slots(params)
|
||||
slot = slots[slot_index]
|
||||
key = slot.get("key")
|
||||
if not slot.get("enabled") or not is_bool_param(params, key):
|
||||
if not slot.get("enabled"):
|
||||
return False
|
||||
|
||||
if is_favorite_action_key(key):
|
||||
return trigger_favorite_action(key, params_memory)
|
||||
|
||||
if not is_bool_param(params, key):
|
||||
return False
|
||||
|
||||
next_value = not params.get_bool(key)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from openpilot.common.params import ParamKeyType
|
||||
from openpilot.starpilot.common.favorite_slots import (
|
||||
FAVORITE_ACTION_ACCEL_COUNTER,
|
||||
FAVORITE_ACTION_DISTANCE_INCREASE,
|
||||
FAVORITE_SLOTS_PARAM,
|
||||
default_favorite_slots,
|
||||
load_favorite_slots,
|
||||
@@ -31,6 +33,12 @@ class FakeParams:
|
||||
def put_bool_nonblocking(self, key, value):
|
||||
self.put_bool(key, value)
|
||||
|
||||
def get_int(self, key, default=0):
|
||||
return int(self.store.get(key, default))
|
||||
|
||||
def put_int(self, key, value):
|
||||
self.store[key] = int(value)
|
||||
|
||||
def get_type(self, key):
|
||||
return self.types.get(key, ParamKeyType.STRING)
|
||||
|
||||
@@ -78,3 +86,14 @@ def test_toggle_favorite_slot_flips_bool_and_requests_refresh():
|
||||
assert toggle_favorite_slot(0, params, memory) is True
|
||||
assert params.get_bool("RedneckCruise") is True
|
||||
assert memory.get_bool("StarPilotTogglesUpdated") is True
|
||||
|
||||
|
||||
def test_toggle_favorite_slot_action_increments_virtual_button_counter():
|
||||
params = FakeParams()
|
||||
memory = FakeParams()
|
||||
params.put(FAVORITE_SLOTS_PARAM, [
|
||||
{"enabled": True, "show_onroad": True, "key": FAVORITE_ACTION_DISTANCE_INCREASE, "label": "Distance + / RES"},
|
||||
])
|
||||
|
||||
assert toggle_favorite_slot(0, params, memory) is True
|
||||
assert memory.get_int(FAVORITE_ACTION_ACCEL_COUNTER) == 1
|
||||
|
||||
@@ -3,7 +3,11 @@ from types import SimpleNamespace
|
||||
from opendbc.car.chrysler.values import CAR as CHRYSLER_CAR
|
||||
|
||||
from openpilot.common.params import ParamKeyType
|
||||
from openpilot.starpilot.common.favorite_slots import FAVORITE_SLOTS_PARAM
|
||||
from openpilot.starpilot.common.favorite_slots import (
|
||||
FAVORITE_ACTION_ACCEL_COUNTER,
|
||||
FAVORITE_ACTION_DISTANCE_INCREASE,
|
||||
FAVORITE_SLOTS_PARAM,
|
||||
)
|
||||
from openpilot.starpilot.controls import starpilot_card as spc
|
||||
|
||||
|
||||
@@ -659,3 +663,18 @@ def test_favorite_wheel_action_toggles_hidden_onroad_slot(monkeypatch, tmp_path)
|
||||
|
||||
assert card.params.get_bool("RedneckCruise") is True
|
||||
assert card.params_memory.get_bool("StarPilotTogglesUpdated") is True
|
||||
|
||||
|
||||
def test_favorite_wheel_action_can_press_virtual_resume(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(spc, "Params", FakeParams)
|
||||
monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False)
|
||||
monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path)
|
||||
|
||||
card = spc.StarPilotCard(SimpleNamespace(brand="gm"), SimpleNamespace(alternativeExperience=0))
|
||||
card.params.put(FAVORITE_SLOTS_PARAM, [
|
||||
{"enabled": True, "show_onroad": False, "key": FAVORITE_ACTION_DISTANCE_INCREASE, "label": "Distance + / RES"},
|
||||
])
|
||||
|
||||
card.handle_button_event("lkas", make_sm(), make_toggles(favorite_1_via_lkas=True))
|
||||
|
||||
assert card.params_memory.get_int(FAVORITE_ACTION_ACCEL_COUNTER) == 1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-1.3.1.js"
|
||||
import { hideSidebar } from "/assets/js/utils.js"
|
||||
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-live-values-1"
|
||||
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-actions-1"
|
||||
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
|
||||
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
|
||||
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
|
||||
|
||||
@@ -533,14 +533,19 @@
|
||||
background: var(--color-gray-950);
|
||||
border: 2px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-family: var(--font-body);
|
||||
gap: var(--gap-base);
|
||||
justify-content: space-between;
|
||||
min-height: 8.5rem;
|
||||
padding: 1.1rem;
|
||||
text-align: left;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ds-favorite-quick-card:hover {
|
||||
@@ -607,6 +612,20 @@
|
||||
transform: translateX(1.75rem);
|
||||
}
|
||||
|
||||
.ds-favorite-action-chip {
|
||||
align-items: center;
|
||||
background: var(--main-fg);
|
||||
border-radius: var(--border-radius-base);
|
||||
color: var(--color-black);
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
justify-content: center;
|
||||
min-width: 4.25rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
}
|
||||
|
||||
.ds-favorite-card {
|
||||
background: var(--input-bg);
|
||||
border: var(--border-style-main);
|
||||
|
||||
@@ -8,6 +8,7 @@ const COLOR_UI_DEFAULTS = {
|
||||
PathColor: "#30ff9c",
|
||||
}
|
||||
const FAVORITE_OPTION_COLLATOR = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" })
|
||||
const FAVORITE_ACTION_PREFIX = "__starpilot_favorite_action__:"
|
||||
const GALAXY_DEVELOPER_MODE_KEY = "GalaxyDeveloperMode"
|
||||
const HIDDEN_SECTION_NAMES = new Set(["Model & Customization"])
|
||||
const HIDDEN_SETTING_KEYS = new Set(["DisableWideRoad", "HumanAcceleration", "ReverseCruise"])
|
||||
@@ -566,6 +567,14 @@ function favoriteOptionMatchesFilter(option, filter) {
|
||||
.some(value => String(value || "").toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
function isFavoriteActionKey(key) {
|
||||
return String(key || "").startsWith(FAVORITE_ACTION_PREFIX)
|
||||
}
|
||||
|
||||
function isFavoriteActionOption(option) {
|
||||
return isFavoriteActionKey(option?.key) || !!option?.action
|
||||
}
|
||||
|
||||
function filteredFavoriteOptions(index) {
|
||||
const filter = state.favoriteFilters[index] || ""
|
||||
return normalizeFavoriteOptions(state.favoriteOptions).filter(opt => favoriteOptionMatchesFilter(opt, filter))
|
||||
@@ -738,6 +747,25 @@ async function updateFavoriteValue(key, checked, sourceEl = null) {
|
||||
}
|
||||
}
|
||||
|
||||
async function activateFavoriteAction(key) {
|
||||
try {
|
||||
const res = await fetch("/api/favorites/action", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key }),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (res.ok) {
|
||||
showParamSnackbar(data.message || "Favorite action sent.")
|
||||
} else {
|
||||
showParamSnackbar(data.error || "Failed to send favorite action", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
showParamSnackbar("Network error — is the device reachable?", "error")
|
||||
}
|
||||
}
|
||||
|
||||
function stepPrecision(step, explicitPrecision) {
|
||||
if (explicitPrecision !== undefined && explicitPrecision !== null && explicitPrecision !== "") {
|
||||
const parsed = Number.parseInt(explicitPrecision, 10)
|
||||
@@ -1248,15 +1276,31 @@ function renderFavoriteSlotsPanel() {
|
||||
const selectedOption = favorite.selectedOption
|
||||
const selectedKey = favorite.selectedKey
|
||||
const selectedValue = favorite.selectedValue
|
||||
const isAction = isFavoriteActionOption(selectedOption)
|
||||
const quickCopy = html`
|
||||
<div class="ds-favorite-quick-copy">
|
||||
<span class="ds-favorite-quick-slot">Favorite #${favorite.index + 1}</span>
|
||||
<span class="ds-favorite-quick-title">${selectedOption.label || favorite.slot.label || selectedKey}</span>
|
||||
${selectedOption.section ? html`<span class="ds-favorite-quick-section">${selectedOption.section}</span>` : ""}
|
||||
${selectedOption.description ? html`<span class="ds-favorite-quick-desc">${selectedOption.description}</span>` : ""}
|
||||
</div>
|
||||
`
|
||||
|
||||
if (isAction) {
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
class="ds-favorite-quick-card ds-favorite-action-card"
|
||||
@click="${() => activateFavoriteAction(selectedKey)}">
|
||||
${quickCopy}
|
||||
<span class="ds-favorite-action-chip">Press</span>
|
||||
</button>
|
||||
`
|
||||
}
|
||||
|
||||
return html`
|
||||
<label class="ds-favorite-quick-card">
|
||||
<div class="ds-favorite-quick-copy">
|
||||
<span class="ds-favorite-quick-slot">Favorite #${favorite.index + 1}</span>
|
||||
<span class="ds-favorite-quick-title">${selectedOption.label || favorite.slot.label || selectedKey}</span>
|
||||
${selectedOption.section ? html`<span class="ds-favorite-quick-section">${selectedOption.section}</span>` : ""}
|
||||
${selectedOption.description ? html`<span class="ds-favorite-quick-desc">${selectedOption.description}</span>` : ""}
|
||||
</div>
|
||||
${quickCopy}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="ds-toggle ds-favorite-quick-toggle"
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=flm-overrides-1">
|
||||
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=favorite-actions-1">
|
||||
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||
|
||||
<script type="module">
|
||||
import("/assets/components/router.js?v=flm-workspace-1").catch((err) => {
|
||||
import("/assets/components/router.js?v=favorite-actions-1").catch((err) => {
|
||||
console.error("[the_galaxy] bootstrap failed", err);
|
||||
const target = document.getElementById("app") || document.body;
|
||||
const pre = document.createElement("pre");
|
||||
|
||||
@@ -140,10 +140,39 @@ def _install_server_import_stubs():
|
||||
sync_persist_chill_state=lambda *args, **kwargs: None,
|
||||
sync_persist_experimental_state=lambda *args, **kwargs: None,
|
||||
)
|
||||
def _trigger_stub_favorite_action(key, params_memory=None):
|
||||
if params_memory is None:
|
||||
return False
|
||||
counter_key = (
|
||||
"FavoriteVirtualAccelCruiseCounter"
|
||||
if str(key or "").endswith("distance_increase")
|
||||
else "FavoriteVirtualDecelCruiseCounter"
|
||||
)
|
||||
params_memory.put_int(counter_key, params_memory.get_int(counter_key) + 1)
|
||||
return True
|
||||
|
||||
sys.modules["openpilot.starpilot.common.favorite_slots"] = _simple_module(
|
||||
"openpilot.starpilot.common.favorite_slots",
|
||||
FAVORITE_ACTION_OPTIONS=(
|
||||
{
|
||||
"key": "__starpilot_favorite_action__:distance_decrease",
|
||||
"label": "Distance - / SET",
|
||||
"description": "Acts like a short press of the car's SET/- cruise button.",
|
||||
"section": "Actions",
|
||||
"action": "decelCruise",
|
||||
},
|
||||
{
|
||||
"key": "__starpilot_favorite_action__:distance_increase",
|
||||
"label": "Distance + / RES",
|
||||
"description": "Acts like a short press of the car's RES/+ cruise button.",
|
||||
"section": "Actions",
|
||||
"action": "accelCruise",
|
||||
},
|
||||
),
|
||||
FAVORITE_SLOTS_PARAM="FavoriteSlots",
|
||||
is_favorite_action_key=lambda key: str(key or "").startswith("__starpilot_favorite_action__:"),
|
||||
normalize_favorite_slots=lambda *args, **kwargs: "",
|
||||
trigger_favorite_action=_trigger_stub_favorite_action,
|
||||
)
|
||||
sys.modules["openpilot.starpilot.common.starpilot_utilities"] = _simple_module(
|
||||
"openpilot.starpilot.common.starpilot_utilities",
|
||||
|
||||
@@ -67,6 +67,13 @@ class WritableFakeParams:
|
||||
self.writes.append((key, bool(value)))
|
||||
self.values[key] = bool(value)
|
||||
|
||||
def get_int(self, key, default=0):
|
||||
return int(self.values.get(key, default))
|
||||
|
||||
def put_int(self, key, value):
|
||||
self.writes.append((key, int(value)))
|
||||
self.values[key] = int(value)
|
||||
|
||||
def remove(self, key):
|
||||
self.removals.append(key)
|
||||
self.values.pop(key, None)
|
||||
@@ -225,6 +232,28 @@ def test_favorite_values_endpoint_returns_current_selected_value(monkeypatch):
|
||||
assert response.get_json() == {"values": {"UseOldUI": False}}
|
||||
|
||||
|
||||
def test_favorite_slot_options_include_virtual_cruise_actions(monkeypatch):
|
||||
monkeypatch.setattr(the_galaxy, "_favorite_slot_options", None)
|
||||
monkeypatch.setattr(the_galaxy, "_get_param_type_info", lambda: (set(), {}))
|
||||
|
||||
options = the_galaxy._get_favorite_slot_options()
|
||||
option_keys = {option["key"] for option in options}
|
||||
|
||||
assert "__starpilot_favorite_action__:distance_decrease" in option_keys
|
||||
assert "__starpilot_favorite_action__:distance_increase" in option_keys
|
||||
|
||||
|
||||
def test_favorite_action_endpoint_increments_virtual_button_counter(monkeypatch):
|
||||
client, _ = _params_client(monkeypatch, {}, "tici")
|
||||
fake_memory = WritableFakeParams()
|
||||
monkeypatch.setattr(the_galaxy, "params_memory", fake_memory)
|
||||
|
||||
response = client.post("/api/favorites/action", json={"key": "__starpilot_favorite_action__:distance_increase"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert fake_memory.get_int("FavoriteVirtualAccelCruiseCounter") == 1
|
||||
|
||||
|
||||
def test_use_old_ui_is_noop_on_c4_mici(monkeypatch):
|
||||
client, fake_params = _params_client(monkeypatch, {"UseOldUI": False, "IsOnroad": False}, "mici")
|
||||
|
||||
|
||||
@@ -62,7 +62,13 @@ from openpilot.starpilot.common.maps_catalog import (
|
||||
schedule_param_value,
|
||||
)
|
||||
from openpilot.starpilot.common.experimental_state import sync_persist_chill_state, sync_persist_experimental_state
|
||||
from openpilot.starpilot.common.favorite_slots import FAVORITE_SLOTS_PARAM, normalize_favorite_slots
|
||||
from openpilot.starpilot.common.favorite_slots import (
|
||||
FAVORITE_ACTION_OPTIONS,
|
||||
FAVORITE_SLOTS_PARAM,
|
||||
is_favorite_action_key,
|
||||
normalize_favorite_slots,
|
||||
trigger_favorite_action,
|
||||
)
|
||||
from openpilot.starpilot.common.lateral_delay import full_lateral_delay
|
||||
from openpilot.starpilot.common.starpilot_utilities import delete_file, get_lock_status, run_cmd
|
||||
from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, LEGACY_STARPILOT_PARAM_RENAMES, MAPS_PATH, MODELS_PATH, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, STOCK_THEME_PATH, THEME_SAVE_PATH,\
|
||||
@@ -2380,6 +2386,7 @@ def _get_favorite_slot_options():
|
||||
|
||||
allowed_keys, value_types = _get_param_type_info()
|
||||
options = []
|
||||
options.extend(dict(option) for option in FAVORITE_ACTION_OPTIONS)
|
||||
try:
|
||||
layout_path = os.path.join(os.path.dirname(__file__), "assets", "components", "tools", "device_settings_layout.json")
|
||||
with open(layout_path) as f:
|
||||
@@ -2415,14 +2422,14 @@ def _favorite_slot_values(options):
|
||||
return {
|
||||
option["key"]: _safe_params_get_bool(option["key"])
|
||||
for option in options
|
||||
if option.get("key")
|
||||
if option.get("key") and not is_favorite_action_key(option.get("key"))
|
||||
}
|
||||
|
||||
def _configured_favorite_slot_values(slots):
|
||||
return {
|
||||
slot["key"]: _safe_params_get_bool(slot["key"])
|
||||
for slot in slots
|
||||
if slot.get("key")
|
||||
if slot.get("key") and not is_favorite_action_key(slot.get("key"))
|
||||
}
|
||||
|
||||
_cached_allowed_keys = None
|
||||
@@ -4211,7 +4218,7 @@ def setup(app):
|
||||
continue
|
||||
key = str(raw_slot.get("key") or "").strip()
|
||||
if key and key not in eligible_keys:
|
||||
return jsonify(error=f"Favorite #{idx + 1} must use a Galaxy-exposed boolean toggle."), 400
|
||||
return jsonify(error=f"Favorite #{idx + 1} must use a Galaxy-exposed toggle or action."), 400
|
||||
|
||||
slots = normalize_favorite_slots(raw_slots, params=params, eligible_keys=eligible_keys)
|
||||
|
||||
@@ -4249,6 +4256,16 @@ def setup(app):
|
||||
slots = normalize_favorite_slots(params.get(FAVORITE_SLOTS_PARAM), params=params, eligible_keys=eligible_keys)
|
||||
return jsonify({"values": _configured_favorite_slot_values(slots)}), 200
|
||||
|
||||
@app.route("/api/favorites/action", methods=["POST"])
|
||||
def favorite_action():
|
||||
data = request.get_json() or {}
|
||||
key = str(data.get("key") or "").strip()
|
||||
if not is_favorite_action_key(key):
|
||||
return jsonify({"error": "Unknown favorite action."}), 400
|
||||
if not trigger_favorite_action(key, params_memory):
|
||||
return jsonify({"error": "Favorite action failed."}), 400
|
||||
return jsonify({"message": "Favorite action sent."}), 200
|
||||
|
||||
@app.route("/api/params", methods=["GET", "PUT"])
|
||||
def get_param():
|
||||
if request.method == "PUT":
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
#include <QJsonObject>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
constexpr int favorite_btn_size = btn_size;
|
||||
constexpr int favorite_indicator_size = 22;
|
||||
constexpr int favorite_slots_count = 3;
|
||||
const QString favorite_action_decrease = "__starpilot_favorite_action__:distance_decrease";
|
||||
const QString favorite_action_increase = "__starpilot_favorite_action__:distance_increase";
|
||||
const std::string favorite_action_decel_counter = "FavoriteVirtualDecelCruiseCounter";
|
||||
const std::string favorite_action_accel_counter = "FavoriteVirtualAccelCruiseCounter";
|
||||
|
||||
QJsonArray parseFavoriteSlots(const std::string &raw_slots) {
|
||||
QJsonParseError error;
|
||||
@@ -117,8 +122,10 @@ FavoriteSlotState FavoriteButton::currentSlot() {
|
||||
next_slot.show_onroad = slot_obj.value("show_onroad").toBool(false);
|
||||
next_slot.key = slot_obj.value("key").toString().trimmed();
|
||||
next_slot.label = slot_obj.value("label").toString().trimmed();
|
||||
next_slot.action = next_slot.key == favorite_action_decrease || next_slot.key == favorite_action_increase;
|
||||
|
||||
if (next_slot.key.isEmpty() || !params.checkKey(next_slot.key.toStdString()) || params.getKeyType(next_slot.key.toStdString()) != ParamKeyType::BOOL) {
|
||||
if (!next_slot.action &&
|
||||
(next_slot.key.isEmpty() || !params.checkKey(next_slot.key.toStdString()) || params.getKeyType(next_slot.key.toStdString()) != ParamKeyType::BOOL)) {
|
||||
next_slot.enabled = false;
|
||||
next_slot.show_onroad = false;
|
||||
next_slot.key.clear();
|
||||
@@ -126,10 +133,12 @@ FavoriteSlotState FavoriteButton::currentSlot() {
|
||||
return next_slot;
|
||||
}
|
||||
|
||||
if (next_slot.label.isEmpty()) {
|
||||
if (next_slot.label.isEmpty() && next_slot.action) {
|
||||
next_slot.label = next_slot.key == favorite_action_increase ? "Distance + / RES" : "Distance - / SET";
|
||||
} else if (next_slot.label.isEmpty()) {
|
||||
next_slot.label = next_slot.key;
|
||||
}
|
||||
next_slot.value = params.getBool(next_slot.key.toStdString());
|
||||
next_slot.value = !next_slot.action && params.getBool(next_slot.key.toStdString());
|
||||
return next_slot;
|
||||
}
|
||||
|
||||
@@ -137,6 +146,7 @@ void FavoriteButton::updateState() {
|
||||
const FavoriteSlotState next_slot = currentSlot();
|
||||
const bool changed = next_slot.enabled != slot.enabled ||
|
||||
next_slot.show_onroad != slot.show_onroad ||
|
||||
next_slot.action != slot.action ||
|
||||
next_slot.value != slot.value ||
|
||||
next_slot.key != slot.key ||
|
||||
next_slot.label != slot.label;
|
||||
@@ -157,6 +167,13 @@ void FavoriteButton::toggleFavorite() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot.action) {
|
||||
const std::string counter_key = slot.key == favorite_action_increase ? favorite_action_accel_counter : favorite_action_decel_counter;
|
||||
params_memory.putInt(counter_key, params_memory.getInt(counter_key) + 1);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool next_value = !params.getBool(slot.key.toStdString());
|
||||
params.putBool(slot.key.toStdString(), next_value);
|
||||
params_memory.putBool("StarPilotTogglesUpdated", true);
|
||||
@@ -193,12 +210,20 @@ void FavoriteButton::paintEvent(QPaintEvent *event) {
|
||||
p.setPen(QPen(QColor(255, 255, 255, isDown() ? 130 : 95), 2));
|
||||
p.drawPath(bg_path);
|
||||
|
||||
const QColor indicator_color = slot.value ? QColor(48, 255, 156) : QColor(135, 135, 135);
|
||||
const QColor indicator_color = slot.action ? QColor(139, 108, 197) : (slot.value ? QColor(48, 255, 156) : QColor(135, 135, 135));
|
||||
const int indicator_x = width() - 20 - favorite_indicator_size;
|
||||
const int indicator_y = 20;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(indicator_color);
|
||||
p.drawEllipse(indicator_x, indicator_y, favorite_indicator_size, favorite_indicator_size);
|
||||
if (slot.action) {
|
||||
QFont action_font = p.font();
|
||||
action_font.setPixelSize(20);
|
||||
action_font.setWeight(QFont::DemiBold);
|
||||
p.setFont(action_font);
|
||||
p.setPen(QColor(255, 255, 255, 230));
|
||||
p.drawText(QRect(indicator_x, indicator_y - 1, favorite_indicator_size, favorite_indicator_size), Qt::AlignCenter, slot.key == favorite_action_increase ? "+" : "-");
|
||||
}
|
||||
|
||||
QFont slot_font = p.font();
|
||||
slot_font.setPixelSize(20);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
struct FavoriteSlotState {
|
||||
bool enabled = false;
|
||||
bool show_onroad = false;
|
||||
bool action = false;
|
||||
bool value = false;
|
||||
QString key;
|
||||
QString label;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from pathlib import Path
|
||||
|
||||
CHESTNUT_VENDOR_ID = 0xADD1
|
||||
CHESTNUT_PRODUCT_ID = 0x0001
|
||||
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
||||
|
||||
|
||||
def read_int(path: Path, base: int = 10) -> int:
|
||||
try:
|
||||
return int(path.read_text(), base)
|
||||
except (OSError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def usb_devices() -> list[Path]:
|
||||
try:
|
||||
devices = (path for path in USB_DEVICES_PATH.glob("*") if (path / "idVendor").exists())
|
||||
return sorted(devices, key=lambda path: path.name)
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def controller(device: Path) -> Path | None:
|
||||
try:
|
||||
return next((parent for parent in device.resolve().parents if parent.name.endswith(".ssusb")), None)
|
||||
except OSError:
|
||||
return None
|
||||
Reference in New Issue
Block a user