mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-25 11:12:13 +08:00
flm update
This commit is contained in:
@@ -56,6 +56,36 @@
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.flmRouteSelection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
}
|
||||
|
||||
.flmSegmentRange {
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--font-size-xs);
|
||||
gap: var(--gap-xs);
|
||||
padding: 0 var(--padding-sm) var(--padding-xs);
|
||||
}
|
||||
|
||||
.flmSegmentRange input {
|
||||
background: var(--input-bg);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--text-color);
|
||||
max-width: 5.5rem;
|
||||
padding: var(--padding-xs);
|
||||
}
|
||||
|
||||
.flmSegmentRange small {
|
||||
color: var(--text-muted);
|
||||
flex: 1 1 10rem;
|
||||
}
|
||||
|
||||
.flmRouteItem,
|
||||
.flmWorkspaceItem {
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -11,6 +11,7 @@ const state = reactive({
|
||||
error: "",
|
||||
routes: [],
|
||||
selectedRoutes: [],
|
||||
segmentRanges: {},
|
||||
truncatedRoutes: false,
|
||||
routeProgress: 0,
|
||||
routeTotal: 0,
|
||||
@@ -42,6 +43,15 @@ function formatTimestamp(value) {
|
||||
return parsed.toLocaleString()
|
||||
}
|
||||
|
||||
function formatReportSegmentRanges(report) {
|
||||
const ranges = report?.segmentRanges || {}
|
||||
const entries = Object.entries(ranges)
|
||||
if (!entries.length) return "Whole selected routes"
|
||||
return entries
|
||||
.map(([route, range]) => `${route}: ${range?.start ?? "first"}-${range?.end ?? "last"}`)
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
function safeCount(value) {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
@@ -326,8 +336,36 @@ function toggleRouteSelection(routeName) {
|
||||
state.selectedRoutes = [...current]
|
||||
}
|
||||
|
||||
function setSegmentRange(routeName, key, value) {
|
||||
const cleaned = String(value ?? "").replace(/[^\d]/g, "")
|
||||
state.segmentRanges = {
|
||||
...state.segmentRanges,
|
||||
[routeName]: {
|
||||
...(state.segmentRanges[routeName] || {}),
|
||||
[key]: cleaned,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function selectedSegmentRanges() {
|
||||
const ranges = {}
|
||||
for (const routeName of state.selectedRoutes) {
|
||||
const selected = state.segmentRanges[routeName] || {}
|
||||
const start = String(selected.start ?? "").trim()
|
||||
const end = String(selected.end ?? "").trim()
|
||||
if (start || end) {
|
||||
ranges[routeName] = {
|
||||
start: start || null,
|
||||
end: end || null,
|
||||
}
|
||||
}
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
function clearSelections() {
|
||||
state.selectedRoutes = []
|
||||
state.segmentRanges = {}
|
||||
}
|
||||
|
||||
async function runAnalyze() {
|
||||
@@ -337,7 +375,10 @@ async function runAnalyze() {
|
||||
const response = await fetch("/api/flm/analyze", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ routes: state.selectedRoutes }),
|
||||
body: JSON.stringify({
|
||||
routes: state.selectedRoutes,
|
||||
segmentRanges: selectedSegmentRanges(),
|
||||
}),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to start tuning analysis.")
|
||||
@@ -1038,7 +1079,7 @@ export function Tuning() {
|
||||
<div>
|
||||
<h3>Local Routes</h3>
|
||||
<p class="longManeuverMuted">
|
||||
Pick up to 8 routes from the device. The analyzer prefers rlogs and falls back to qlogs when needed.
|
||||
Pick up to 8 routes from the device. Optionally limit each route to a segment range. The analyzer prefers rlogs and falls back to qlogs when needed.
|
||||
</p>
|
||||
</div>
|
||||
<button class="longManeuverButton" @click="${clearSelections}">Clear</button>
|
||||
@@ -1050,23 +1091,46 @@ export function Tuning() {
|
||||
|
||||
<div class="flmRouteList">
|
||||
${() => sortedRoutes().map((route) => html`
|
||||
<div class="flmRouteRow">
|
||||
<label class="flmRouteItem">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked="${() => state.selectedRoutes.includes(route.name)}"
|
||||
@change="${() => toggleRouteSelection(route.name)}" />
|
||||
<span>
|
||||
<strong>${route.timestampLabel}</strong>
|
||||
<small>${route.name}</small>
|
||||
</span>
|
||||
</label>
|
||||
${() => connectRouteUrl(route.name) ? html`
|
||||
<a
|
||||
class="flmConnectLink"
|
||||
href="${connectRouteUrl(route.name)}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Connect</a>
|
||||
<div class="flmRouteSelection">
|
||||
<div class="flmRouteRow">
|
||||
<label class="flmRouteItem">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked="${() => state.selectedRoutes.includes(route.name)}"
|
||||
@change="${() => toggleRouteSelection(route.name)}" />
|
||||
<span>
|
||||
<strong>${route.timestampLabel}</strong>
|
||||
<small>${route.name}</small>
|
||||
</span>
|
||||
</label>
|
||||
${() => connectRouteUrl(route.name) ? html`
|
||||
<a
|
||||
class="flmConnectLink"
|
||||
href="${connectRouteUrl(route.name)}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Connect</a>
|
||||
` : ""}
|
||||
</div>
|
||||
${() => state.selectedRoutes.includes(route.name) ? html`
|
||||
<div class="flmSegmentRange">
|
||||
<span>Segments</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
inputmode="numeric"
|
||||
placeholder="First"
|
||||
value="${() => state.segmentRanges[route.name]?.start || ""}"
|
||||
@input="${event => setSegmentRange(route.name, "start", event.target.value)}" />
|
||||
<span>to</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
inputmode="numeric"
|
||||
placeholder="Last"
|
||||
value="${() => state.segmentRanges[route.name]?.end || ""}"
|
||||
@input="${event => setSegmentRange(route.name, "end", event.target.value)}" />
|
||||
<small>Blank analyzes the whole route.</small>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
`)}
|
||||
@@ -1133,7 +1197,9 @@ export function Tuning() {
|
||||
<p><strong>Path Choice:</strong> ${state.report.pathSelectionSource === "manual" ? "Manual override" : "Automatic"}</p>
|
||||
<p><strong>Nonlinear Torque Map:</strong> ${state.report.capabilities?.nonlinearTorqueMap?.type === "siglin" ? (state.report.capabilities.nonlinearTorqueMap.asymmetric ? "Asymmetric left/right siglin" : "Symmetric siglin") : "Not detected"}</p>
|
||||
<p><strong>Live Learner Refits Map:</strong> ${state.report.capabilities?.nonlinearTorqueMap?.type === "siglin" ? "No" : "Not applicable"}</p>
|
||||
<p><strong>Segment Selection:</strong> ${formatReportSegmentRanges(state.report)}</p>
|
||||
<p><strong>Processed Segments:</strong> ${safeCount(state.report.summary?.processedSegments)}</p>
|
||||
<p><strong>Skipped Segments:</strong> ${safeCount(state.report.summary?.skippedSegments)}</p>
|
||||
<p><strong>Driver-Override Samples Excluded:</strong> ${safeCount(state.report.summary?.excludedDriverOverrideSamples)}</p>
|
||||
<p><strong>qlog Fallback:</strong> ${state.report.summary?.usedQlogFallback ? "Yes" : "No"}</p>
|
||||
<p><strong>Samples:</strong> ${safeCount(state.report.summary?.sampleCount)}</p>
|
||||
|
||||
@@ -46,12 +46,17 @@ FLM_ANALYZER_PROCESS = None
|
||||
FLM_ANALYZER_LOCK = threading.Lock()
|
||||
FLM_PROGRESS_FILENAME = "progress.json"
|
||||
FLM_ONROAD_POLL_INTERVAL_SECONDS = 0.25
|
||||
FLM_SEGMENT_TIMEOUT_SECONDS = 180.0
|
||||
|
||||
|
||||
class FLMAnalysisCancelled(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class FLMSegmentTimeout(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
TRIAL_PARAM_SPECS = {
|
||||
"AdvancedLateralTune": "bool",
|
||||
"ForceAutoTune": "bool",
|
||||
@@ -133,7 +138,7 @@ FLM_DRIVER_OVERRIDE_PRE_BUFFER_S = 0.35
|
||||
FLM_DRIVER_OVERRIDE_POST_BUFFER_S = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class RouteSource:
|
||||
route: str
|
||||
footage_path: str
|
||||
@@ -143,7 +148,7 @@ class RouteSource:
|
||||
used_qlog: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class FLMSample:
|
||||
route: str
|
||||
segment: int
|
||||
@@ -513,12 +518,42 @@ def cancel_flm_if_onroad() -> bool:
|
||||
return stop_flm_background_analysis(reason="onroad")
|
||||
|
||||
|
||||
def start_flm_background_analysis(route_names: list[str], footage_paths: list[str]) -> bool:
|
||||
def _optional_segment_number(value: Any) -> int | None:
|
||||
if value is None or str(value).strip() == "":
|
||||
return None
|
||||
number = int(value)
|
||||
if number < 0:
|
||||
raise ValueError("Segment numbers cannot be negative.")
|
||||
return number
|
||||
|
||||
|
||||
def normalize_segment_ranges(route_names: list[str], segment_ranges: Any) -> dict[str, dict[str, int | None]]:
|
||||
if not isinstance(segment_ranges, dict):
|
||||
return {}
|
||||
|
||||
normalized: dict[str, dict[str, int | None]] = {}
|
||||
valid_routes = set(route_names)
|
||||
for route, raw_range in segment_ranges.items():
|
||||
route_name = str(route).strip()
|
||||
if route_name not in valid_routes or not isinstance(raw_range, dict):
|
||||
continue
|
||||
start = _optional_segment_number(raw_range.get("start"))
|
||||
end = _optional_segment_number(raw_range.get("end"))
|
||||
if start is not None and end is not None and start > end:
|
||||
raise ValueError(f"{route_name}: the first segment cannot be after the last segment.")
|
||||
if start is not None or end is not None:
|
||||
normalized[route_name] = {"start": start, "end": end}
|
||||
return normalized
|
||||
|
||||
|
||||
def start_flm_background_analysis(route_names: list[str], footage_paths: list[str],
|
||||
segment_ranges: dict[str, dict[str, int | None]] | None = None) -> bool:
|
||||
global FLM_ANALYZER_PROCESS
|
||||
|
||||
route_names = [str(route) for route in route_names if str(route).strip()]
|
||||
if not route_names:
|
||||
return False
|
||||
segment_ranges = normalize_segment_ranges(route_names, segment_ranges)
|
||||
try:
|
||||
_require_flm_offroad()
|
||||
except FLMAnalysisCancelled:
|
||||
@@ -541,6 +576,11 @@ def start_flm_background_analysis(route_names: list[str], footage_paths: list[st
|
||||
json.dumps({
|
||||
"routes": route_names[:FLM_ANALYZER_ROUTE_LIMIT],
|
||||
"footagePaths": [str(path) for path in footage_paths],
|
||||
"segmentRanges": {
|
||||
route: segment_range
|
||||
for route, segment_range in segment_ranges.items()
|
||||
if route in route_names[:FLM_ANALYZER_ROUTE_LIMIT]
|
||||
},
|
||||
}),
|
||||
]
|
||||
log_file = None
|
||||
@@ -561,6 +601,7 @@ def start_flm_background_analysis(route_names: list[str], footage_paths: list[st
|
||||
"running": True,
|
||||
"state": "queued",
|
||||
"routes": route_names[:FLM_ANALYZER_ROUTE_LIMIT],
|
||||
"segmentRanges": segment_ranges,
|
||||
"progress": 0,
|
||||
"total": len(route_names[:FLM_ANALYZER_ROUTE_LIMIT]),
|
||||
})
|
||||
@@ -584,16 +625,26 @@ def _parse_segment_num(segment_name: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def resolve_route_sources(route_names: list[str], footage_paths: list[str]) -> tuple[list[RouteSource], list[str]]:
|
||||
def resolve_route_sources(route_names: list[str], footage_paths: list[str],
|
||||
segment_ranges: dict[str, dict[str, int | None]] | None = None) -> tuple[list[RouteSource], list[str]]:
|
||||
sources: list[RouteSource] = []
|
||||
warnings: list[str] = []
|
||||
segment_ranges = normalize_segment_ranges(route_names, segment_ranges)
|
||||
for route in route_names:
|
||||
route_added = False
|
||||
segment_range = segment_ranges.get(route, {})
|
||||
segment_start = segment_range.get("start")
|
||||
segment_end = segment_range.get("end")
|
||||
for footage_path in footage_paths:
|
||||
segments = utilities.get_segments_in_route(route, footage_path)
|
||||
if not segments:
|
||||
continue
|
||||
for segment in segments:
|
||||
segment_num = _parse_segment_num(segment)
|
||||
if segment_start is not None and segment_num < segment_start:
|
||||
continue
|
||||
if segment_end is not None and segment_num > segment_end:
|
||||
continue
|
||||
segment_path = Path(footage_path) / segment
|
||||
rlog_path = None
|
||||
qlog_path = None
|
||||
@@ -616,7 +667,7 @@ def resolve_route_sources(route_names: list[str], footage_paths: list[str]) -> t
|
||||
route=route,
|
||||
footage_path=str(footage_path),
|
||||
segment=segment,
|
||||
segment_num=_parse_segment_num(segment),
|
||||
segment_num=segment_num,
|
||||
log_path=str(log_path),
|
||||
used_qlog=rlog_path is None,
|
||||
))
|
||||
@@ -624,7 +675,12 @@ def resolve_route_sources(route_names: list[str], footage_paths: list[str]) -> t
|
||||
if route_added:
|
||||
break
|
||||
if not route_added:
|
||||
warnings.append(f"{route} could not be resolved to a local route with logs.")
|
||||
if segment_range:
|
||||
start_label = segment_start if segment_start is not None else "first"
|
||||
end_label = segment_end if segment_end is not None else "last"
|
||||
warnings.append(f"{route} had no local logs in the selected segment range {start_label}-{end_label}.")
|
||||
else:
|
||||
warnings.append(f"{route} could not be resolved to a local route with logs.")
|
||||
sources.sort(key=lambda source: (source.route, source.segment_num))
|
||||
return sources, warnings
|
||||
|
||||
@@ -874,7 +930,7 @@ def _segment_samples(segment_source: RouteSource, params: Params | None = None)
|
||||
|
||||
_require_flm_offroad(params)
|
||||
|
||||
for msg in LogReader(segment_source.log_path, sort_by_time=True):
|
||||
for msg in LogReader(segment_source.log_path):
|
||||
now = time.monotonic()
|
||||
if now - last_offroad_check >= FLM_ONROAD_POLL_INTERVAL_SECONDS:
|
||||
_require_flm_offroad(params)
|
||||
@@ -950,6 +1006,33 @@ def _segment_samples(segment_source: RouteSource, params: Params | None = None)
|
||||
return samples, car_params, init_data, control_states
|
||||
|
||||
|
||||
def _segment_samples_with_timeout(segment_source: RouteSource, params: Params,
|
||||
timeout_seconds: float = FLM_SEGMENT_TIMEOUT_SECONDS):
|
||||
if (
|
||||
timeout_seconds <= 0.0
|
||||
or threading.current_thread() is not threading.main_thread()
|
||||
or not hasattr(signal, "SIGALRM")
|
||||
or not hasattr(signal, "setitimer")
|
||||
):
|
||||
return _segment_samples(segment_source, params=params)
|
||||
|
||||
def handle_timeout(_signum, _frame):
|
||||
raise FLMSegmentTimeout(
|
||||
f"{segment_source.route} segment {segment_source.segment_num} exceeded the {timeout_seconds:.0f}-second read limit."
|
||||
)
|
||||
|
||||
previous_handler = signal.getsignal(signal.SIGALRM)
|
||||
signal.signal(signal.SIGALRM, handle_timeout)
|
||||
previous_timer = signal.setitimer(signal.ITIMER_REAL, timeout_seconds)
|
||||
try:
|
||||
return _segment_samples(segment_source, params=params)
|
||||
finally:
|
||||
signal.setitimer(signal.ITIMER_REAL, 0.0)
|
||||
signal.signal(signal.SIGALRM, previous_handler)
|
||||
if previous_timer[0] > 0.0:
|
||||
signal.setitimer(signal.ITIMER_REAL, previous_timer[0], previous_timer[1])
|
||||
|
||||
|
||||
def _current_param_state(CP, params: Params) -> dict[str, Any]:
|
||||
advanced_enabled = params.get_bool("AdvancedLateralTune")
|
||||
torque_tune = CP.lateralTuning.torque if CP.lateralTuning.which() == "torque" else None
|
||||
@@ -2133,14 +2216,16 @@ def _render_report_html(report: dict[str, Any]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: dict[str, Any] | None = None, report_id: str | None = None) -> dict[str, Any]:
|
||||
def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: dict[str, Any] | None = None,
|
||||
report_id: str | None = None, segment_ranges: dict[str, dict[str, int | None]] | None = None) -> dict[str, Any]:
|
||||
ensure_flm_workspace()
|
||||
params = Params(return_defaults=True)
|
||||
_require_flm_offroad(params)
|
||||
report_id = report_id or f"flm-{int(time.time())}"
|
||||
feedback = feedback or {}
|
||||
segment_ranges = normalize_segment_ranges(route_names, segment_ranges)
|
||||
|
||||
sources, warnings = resolve_route_sources(route_names, footage_paths)
|
||||
sources, warnings = resolve_route_sources(route_names, footage_paths, segment_ranges)
|
||||
if not sources:
|
||||
raise RuntimeError("No local routes with qlogs or rlogs were found for the selected routes.")
|
||||
|
||||
@@ -2150,6 +2235,7 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
observed_control_states: dict[str, int] = {}
|
||||
used_qlog = False
|
||||
processed_segments = 0
|
||||
skipped_segments = 0
|
||||
for idx, source in enumerate(sources, start=1):
|
||||
_require_flm_offroad(params)
|
||||
_write_flm_status({
|
||||
@@ -2158,11 +2244,25 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
"running": True,
|
||||
"state": "analyzing",
|
||||
"routes": route_names,
|
||||
"segmentRanges": segment_ranges,
|
||||
"progress": idx - 1,
|
||||
"total": len(sources),
|
||||
"currentSegment": source.segment,
|
||||
})
|
||||
segment_samples, segment_car_params, segment_init, segment_control_states = _segment_samples(source, params=params)
|
||||
try:
|
||||
segment_samples, segment_car_params, segment_init, segment_control_states = _segment_samples_with_timeout(source, params)
|
||||
except FLMAnalysisCancelled:
|
||||
raise
|
||||
except FLMSegmentTimeout as error:
|
||||
warnings.append(str(error) + " The segment was skipped.")
|
||||
skipped_segments += 1
|
||||
continue
|
||||
except Exception as error:
|
||||
warnings.append(
|
||||
f"{source.route} segment {source.segment_num} could not be read ({type(error).__name__}). The segment was skipped."
|
||||
)
|
||||
skipped_segments += 1
|
||||
continue
|
||||
_require_flm_offroad(params)
|
||||
if segment_car_params is not None:
|
||||
car_params_candidates.append(segment_car_params)
|
||||
@@ -2293,6 +2393,7 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
"reportId": report_id,
|
||||
"createdAt": time.time(),
|
||||
"routeNames": route_names,
|
||||
"segmentRanges": segment_ranges,
|
||||
"warnings": warnings,
|
||||
"feedback": feedback,
|
||||
"car": {
|
||||
@@ -2311,6 +2412,7 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
"summary": {
|
||||
**summary_stats,
|
||||
"processedSegments": processed_segments,
|
||||
"skippedSegments": skipped_segments,
|
||||
"usedQlogFallback": used_qlog,
|
||||
},
|
||||
"primaryPathKey": path_decision["primaryPathKey"],
|
||||
@@ -2342,8 +2444,10 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
"running": False,
|
||||
"state": "complete",
|
||||
"routes": route_names,
|
||||
"progress": processed_segments,
|
||||
"total": processed_segments,
|
||||
"segmentRanges": segment_ranges,
|
||||
"progress": len(sources),
|
||||
"total": len(sources),
|
||||
"skippedSegments": skipped_segments,
|
||||
"reportId": report_id,
|
||||
})
|
||||
return report
|
||||
@@ -2875,6 +2979,7 @@ def run_worker(payload_json: str) -> None:
|
||||
payload = json.loads(payload_json)
|
||||
routes = [str(route) for route in payload.get("routes", [])]
|
||||
footage_paths = [str(path) for path in payload.get("footagePaths", [])]
|
||||
segment_ranges = normalize_segment_ranges(routes, payload.get("segmentRanges", {}))
|
||||
ensure_flm_workspace()
|
||||
_write_flm_status({
|
||||
"pid": os.getpid(),
|
||||
@@ -2882,21 +2987,27 @@ def run_worker(payload_json: str) -> None:
|
||||
"running": True,
|
||||
"state": "starting",
|
||||
"routes": routes,
|
||||
"segmentRanges": segment_ranges,
|
||||
"progress": 0,
|
||||
"total": len(routes),
|
||||
})
|
||||
threading.Thread(target=_watch_flm_worker_for_onroad, daemon=True).start()
|
||||
try:
|
||||
_require_flm_offroad()
|
||||
report = analyze_routes(routes, footage_paths)
|
||||
report = analyze_routes(routes, footage_paths, segment_ranges=segment_ranges)
|
||||
processed_segments = int(report.get("summary", {}).get("processedSegments", 0) or 0)
|
||||
skipped_segments = int(report.get("summary", {}).get("skippedSegments", 0) or 0)
|
||||
total_segments = processed_segments + skipped_segments
|
||||
_write_flm_status({
|
||||
"pid": os.getpid(),
|
||||
"startedAt": time.time(),
|
||||
"running": False,
|
||||
"state": "complete",
|
||||
"routes": routes,
|
||||
"progress": len(routes),
|
||||
"total": len(routes),
|
||||
"segmentRanges": segment_ranges,
|
||||
"progress": total_segments,
|
||||
"total": total_segments,
|
||||
"skippedSegments": skipped_segments,
|
||||
"reportId": report["reportId"],
|
||||
})
|
||||
except FLMAnalysisCancelled as error:
|
||||
@@ -2906,6 +3017,7 @@ def run_worker(payload_json: str) -> None:
|
||||
"running": False,
|
||||
"state": "cancelled_onroad",
|
||||
"routes": routes,
|
||||
"segmentRanges": segment_ranges,
|
||||
"progress": 0,
|
||||
"total": len(routes),
|
||||
"error": str(error),
|
||||
@@ -2917,6 +3029,7 @@ def run_worker(payload_json: str) -> None:
|
||||
"running": False,
|
||||
"state": "failed",
|
||||
"routes": routes,
|
||||
"segmentRanges": segment_ranges,
|
||||
"progress": 0,
|
||||
"total": len(routes),
|
||||
"error": str(error),
|
||||
|
||||
@@ -1419,6 +1419,20 @@ def _load_server_module():
|
||||
return module
|
||||
|
||||
|
||||
def test_troubleshoot_steer_delay_normalizes_vehicle_delay_for_display():
|
||||
server = _load_server_module()
|
||||
|
||||
assert server._normalize_troubleshoot_current_display_value("SteerDelay", 0.1, 0.3) == 0.3
|
||||
assert server._normalize_troubleshoot_current_display_value("SteerDelay", 0.3, 0.3) == 0.3
|
||||
|
||||
|
||||
def test_troubleshoot_steer_delay_preserves_real_custom_values():
|
||||
server = _load_server_module()
|
||||
|
||||
assert server._normalize_troubleshoot_current_display_value("SteerDelay", 0.2, 0.3) == 0.2
|
||||
assert server._normalize_troubleshoot_current_display_value("LongitudinalActuatorDelay", 0.1, 0.3) == 0.1
|
||||
|
||||
|
||||
def test_stats_endpoint_keeps_existing_keys_and_adds_dashboard(monkeypatch):
|
||||
server = _load_server_module()
|
||||
assert server._import_galaxy_web_symbols()
|
||||
|
||||
@@ -2,6 +2,7 @@ import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
@@ -210,6 +211,48 @@ def test_effective_control_path_keeps_true_angle_and_mixed_routes_diagnostic(tmp
|
||||
assert module._effective_control_path(angle_cp, {"angleState": 900, "torqueState": 900}) == ("mixed", "controlsState")
|
||||
|
||||
|
||||
def test_segment_ranges_limit_resolved_route_sources(tmp_path, monkeypatch):
|
||||
module, _ = _load_flm_workspace_module(tmp_path)
|
||||
route = "00000001--abcdef1234"
|
||||
segment_names = [f"{route}--{segment}" for segment in range(12)]
|
||||
for segment_name in segment_names:
|
||||
segment_path = tmp_path / segment_name
|
||||
segment_path.mkdir()
|
||||
(segment_path / "rlog.zst").write_bytes(b"log")
|
||||
|
||||
monkeypatch.setattr(module.utilities, "get_segments_in_route", lambda *_args: segment_names)
|
||||
sources, warnings = module.resolve_route_sources(
|
||||
[route],
|
||||
[str(tmp_path)],
|
||||
{route: {"start": 4, "end": 9}},
|
||||
)
|
||||
|
||||
assert [source.segment_num for source in sources] == [4, 5, 6, 7, 8, 9]
|
||||
assert warnings == []
|
||||
|
||||
|
||||
def test_segment_range_rejects_reversed_bounds(tmp_path):
|
||||
module, _ = _load_flm_workspace_module(tmp_path)
|
||||
with pytest.raises(ValueError, match="first segment"):
|
||||
module.normalize_segment_ranges(["route"], {"route": {"start": 9, "end": 4}})
|
||||
|
||||
|
||||
def test_segment_reader_timeout_interrupts_stalled_log(tmp_path, monkeypatch):
|
||||
module, _ = _load_flm_workspace_module(tmp_path)
|
||||
source = module.RouteSource(
|
||||
route="route",
|
||||
footage_path=str(tmp_path),
|
||||
segment="route--41",
|
||||
segment_num=41,
|
||||
log_path=str(tmp_path / "route--41" / "rlog.zst"),
|
||||
used_qlog=False,
|
||||
)
|
||||
monkeypatch.setattr(module, "_segment_samples", lambda *_args, **_kwargs: time.sleep(0.2))
|
||||
|
||||
with pytest.raises(module.FLMSegmentTimeout, match="segment 41"):
|
||||
module._segment_samples_with_timeout(source, module.Params(), timeout_seconds=0.02)
|
||||
|
||||
|
||||
def test_analysis_is_rejected_while_onroad(tmp_path):
|
||||
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
||||
fake_params_cls._store = {"IsOnroad": True}
|
||||
|
||||
@@ -2916,6 +2916,20 @@ def _resolve_troubleshoot_default_value(key, value_type, default_values):
|
||||
|
||||
return _coerce_param_value(default_raw_value, safe_type)
|
||||
|
||||
def _normalize_troubleshoot_current_display_value(key, current_value, default_value):
|
||||
if key != "SteerDelay":
|
||||
return current_value
|
||||
|
||||
try:
|
||||
full_current_delay = full_lateral_delay(float(current_value))
|
||||
numeric_default = float(default_value)
|
||||
except (TypeError, ValueError):
|
||||
return current_value
|
||||
|
||||
if math.isfinite(full_current_delay) and math.isfinite(numeric_default) and math.isclose(full_current_delay, numeric_default, abs_tol=1e-6):
|
||||
return default_value
|
||||
return current_value
|
||||
|
||||
def _normalize_live_delay_status(status):
|
||||
status_text = str(status or "").strip().lower()
|
||||
if status_text in {"estimated", "unestimated", "invalid"}:
|
||||
@@ -3245,6 +3259,7 @@ def _build_troubleshoot_section_payload(section_definition, value_types, default
|
||||
try:
|
||||
current_value = _resolve_troubleshoot_current_value(key, value_type, default_values)
|
||||
default_value = _resolve_troubleshoot_default_value(key, value_type, default_values)
|
||||
current_value = _normalize_troubleshoot_current_display_value(key, current_value, default_value)
|
||||
except Exception:
|
||||
current_value = "Unavailable"
|
||||
default_value = "n/a"
|
||||
@@ -5918,7 +5933,12 @@ def setup(app):
|
||||
if not route_names:
|
||||
return jsonify({"error": "No routes were selected."}), 400
|
||||
|
||||
started = flm_workspace.start_flm_background_analysis(route_names, FOOTAGE_PATHS)
|
||||
try:
|
||||
segment_ranges = flm_workspace.normalize_segment_ranges(route_names, data.get("segmentRanges", {}))
|
||||
except (TypeError, ValueError) as error:
|
||||
return jsonify({"error": str(error)}), 400
|
||||
|
||||
started = flm_workspace.start_flm_background_analysis(route_names, FOOTAGE_PATHS, segment_ranges)
|
||||
if not started:
|
||||
return jsonify({"error": "Failed to start FLM analysis."}), 500
|
||||
|
||||
|
||||
Reference in New Issue
Block a user