Extract decoder runtime and remove dead timing helpers

This commit is contained in:
larssand
2026-03-26 19:39:56 +01:00
parent bee5a8453a
commit f5d13cf2f8
2 changed files with 440 additions and 1100 deletions

1173
src/app.js

File diff suppressed because it is too large Load Diff

367
src/decoder_runtime.js Normal file
View File

@@ -0,0 +1,367 @@
function parseRtcTime(value) {
if (!value || typeof value !== "string") {
return null;
}
const ts = Date.parse(value);
return Number.isFinite(ts) ? ts : null;
}
function resolveCompetitor(session, transponder, deps) {
const { state, t, findEventTeamForPassing } = deps;
const sessionType = String(session?.type || "").toLowerCase();
const isOpenPractice = sessionType === "open_practice";
const isFreePractice = sessionType === "free_practice";
const isOpenMonitoringSession = isOpenPractice || isFreePractice;
const event = state.events.find((item) => item.id === session?.eventId) || null;
if (session.mode === "track") {
const matchingAssignments = (session.assignments || []).filter((a) => {
const car = state.cars.find((c) => c.id === a.carId);
return car?.transponder === transponder;
});
if (matchingAssignments.length > 1) {
return {
key: `track_ambiguous_${transponder}`,
driverId: null,
driverName: `Ambiguous TP ${transponder}`,
carId: null,
carName: t("common.unknown_car"),
};
}
const assignment = matchingAssignments[0];
if (assignment) {
const driver = state.drivers.find((d) => d.id === assignment.driverId);
const car = state.cars.find((c) => c.id === assignment.carId);
return {
key: `track_${assignment.id}`,
driverId: driver?.id || null,
driverName: driver?.name || t("common.unknown_driver"),
carId: car?.id || null,
carName: car?.name || t("common.unknown_car"),
};
}
return {
key: `track_tp_${transponder}`,
driverId: null,
driverName: t("common.unassigned_driver"),
carId: null,
carName: t("common.unknown_car"),
};
}
if (session.mode === "race" && sessionType === "team_race") {
const driver = state.drivers.find((d) => d.transponder === transponder) || null;
const car = state.cars.find((c) => c.transponder === transponder) || null;
const team = event ? findEventTeamForPassing(event, driver?.id || null, car?.id || null) : null;
if (!team) {
return {
key: `ignore_team_${transponder}`,
ignore: true,
};
}
const memberBits = [driver?.name || "", car?.name || ""].filter(Boolean);
return {
key: `team_${team.id}`,
teamId: team.id,
teamName: team.name,
displayName: team.name,
subLabel: memberBits.join(" • ") || transponder,
driverId: driver?.id || null,
driverName: driver?.name || team.name,
carId: car?.id || null,
carName: car?.name || t("common.unknown_car"),
};
}
const driver = state.drivers.find((d) => d.transponder === transponder);
if (driver) {
if (!isOpenMonitoringSession && Array.isArray(session.driverIds) && session.driverIds.length && !session.driverIds.includes(driver.id)) {
return {
key: `ignore_${driver.id}`,
ignore: true,
};
}
return {
key: `driver_${driver.id}`,
driverId: driver.id,
driverName: driver.name,
displayName: driver.name,
subLabel: driver.transponder || "",
carId: null,
carName: t("common.driver_car"),
};
}
return {
key: `driver_tp_${transponder}`,
driverId: null,
driverName: isOpenPractice ? transponder : isFreePractice ? `TP ${transponder}` : t("common.unknown_driver"),
displayName: isOpenPractice ? transponder : isFreePractice ? `TP ${transponder}` : t("common.unknown_driver"),
subLabel: transponder,
carId: null,
carName: t("common.unknown_car"),
};
}
export function connectDecoderHelper(deps) {
const {
state, t, saveState, getWsClient, setWsClient, getReconnectTimer, setReconnectTimer,
getCurrentView, renderView, updateConnectionBadge, processDecoderMessage, disconnectDecoder,
} = deps;
disconnectDecoder({ silent: true });
state.decoder.lastError = "";
saveState();
const url = state.settings.wsUrl;
try {
setWsClient(new WebSocket(url));
} catch (error) {
state.decoder.lastError = t("error.ws_invalid", { msg: error instanceof Error ? error.message : String(error) });
saveState();
renderView();
return;
}
const wsClient = getWsClient();
wsClient.onopen = () => {
state.decoder.connected = true;
state.decoder.lastError = "";
saveState();
updateConnectionBadge();
if (["timing", "settings", "overlay"].includes(getCurrentView())) {
renderView();
}
};
wsClient.onmessage = (event) => {
state.decoder.lastMessageAt = Date.now();
let parsed;
try {
parsed = JSON.parse(String(event.data));
} catch {
return;
}
if (Array.isArray(parsed)) {
parsed.forEach(processDecoderMessage);
} else {
processDecoderMessage(parsed);
}
saveState();
updateConnectionBadge();
if (["timing", "dashboard", "overlay"].includes(getCurrentView())) {
renderView();
}
};
wsClient.onclose = () => {
state.decoder.connected = false;
saveState();
updateConnectionBadge();
if (state.settings.autoReconnect) {
clearTimeout(getReconnectTimer());
setReconnectTimer(setTimeout(() => {
if (!state.decoder.connected) {
connectDecoderHelper(deps);
}
}, 2000));
}
if (["timing", "settings", "overlay"].includes(getCurrentView())) {
renderView();
}
};
wsClient.onerror = () => {
state.decoder.lastError = t("error.decoder_connection");
saveState();
updateConnectionBadge();
if (["timing", "settings", "overlay"].includes(getCurrentView())) {
renderView();
}
};
}
export function disconnectDecoderHelper(options = {}, deps) {
const { state, saveState, getWsClient, setWsClient, getReconnectTimer, updateConnectionBadge } = deps;
clearTimeout(getReconnectTimer());
const wsClient = getWsClient();
if (wsClient) {
wsClient.onopen = null;
wsClient.onmessage = null;
wsClient.onclose = null;
wsClient.onerror = null;
wsClient.close();
setWsClient(null);
}
state.decoder.connected = false;
if (!options.silent) {
saveState();
}
updateConnectionBadge();
}
export function processDecoderMessageHelper(msg, deps) {
const {
state, t, getActiveSession, ensureSessionResult, normalizeStartMode, getSessionLapWindow,
findEventTeamForPassing, persistPassingToBackend, pushOverlayEvent, buildLeaderboard,
formatLap, announcePassing, saveState, lastOverlayLeaderKeyBySession,
lastOverlayTop3BySession, lastOverlayBestLapByKey,
} = deps;
if (!msg || typeof msg !== "object") {
return;
}
const type = String(msg.msg || msg.type || "").toUpperCase();
if (type !== "PASSING") {
return;
}
const session = getActiveSession();
if (!session || session.status !== "running") {
return;
}
const timestamp = parseRtcTime(msg.rtc_time) || Date.now();
const transponder = String(msg.transponder ?? msg.tran_code ?? "").replace("ID:", "").trim();
if (!transponder) {
return;
}
const result = ensureSessionResult(session.id);
const competitor = resolveCompetitor(session, transponder, { state, t, findEventTeamForPassing });
if (competitor.ignore) {
return;
}
const key = competitor.key;
if (!result.competitors[key]) {
result.competitors[key] = {
key,
teamId: competitor.teamId || null,
teamName: competitor.teamName || "",
driverId: competitor.driverId,
driverName: competitor.driverName,
displayName: competitor.displayName || competitor.driverName,
subLabel: competitor.subLabel || competitor.carName || "",
carId: competitor.carId,
carName: competitor.carName,
transponder,
laps: 0,
lastLapMs: null,
bestLapMs: null,
startTimestamp: null,
lastTimestamp: null,
};
}
const entry = result.competitors[key];
entry.teamId = competitor.teamId || entry.teamId || null;
entry.teamName = competitor.teamName || entry.teamName || "";
entry.displayName = competitor.displayName || entry.displayName || competitor.driverName;
entry.subLabel = competitor.subLabel || entry.subLabel || competitor.carName || "";
entry.driverId = competitor.driverId ?? entry.driverId;
entry.driverName = competitor.driverName || entry.driverName;
entry.carId = competitor.carId ?? entry.carId;
entry.carName = competitor.carName || entry.carName;
entry.transponder = transponder;
const startMode = normalizeStartMode(session.startMode);
if (startMode === "staggered" && !entry.startTimestamp) {
entry.startTimestamp = timestamp;
entry.lastTimestamp = timestamp;
announcePassing(entry);
saveState();
return;
}
if (!entry.startTimestamp) {
entry.startTimestamp = session.startedAt || timestamp;
}
const baseTs = entry.lastTimestamp || entry.startTimestamp || session.startedAt || timestamp;
const lapMs = Math.max(0, timestamp - baseTs);
const { minLapMs, maxLapMs } = getSessionLapWindow(session);
let validLap = true;
let invalidReason = "";
if (minLapMs > 0 && lapMs > 0 && lapMs < minLapMs) {
validLap = false;
invalidReason = "below_min";
} else if (Number.isFinite(maxLapMs) && maxLapMs > 0 && lapMs > maxLapMs) {
validLap = false;
invalidReason = "above_max";
}
const passing = {
timestamp,
transponder,
teamId: entry.teamId,
teamName: entry.teamName,
driverId: entry.driverId,
driverName: entry.driverName,
displayName: entry.displayName,
subLabel: entry.subLabel,
carId: entry.carId,
carName: entry.carName,
competitorKey: key,
lapMs,
validLap,
invalidReason,
strength: msg.strength,
loopId: String(msg.loop_id || ""),
resend: Boolean(msg.resend),
};
if (!validLap) {
result.passings.push(passing);
if (invalidReason === "above_max") {
entry.lastTimestamp = timestamp;
}
persistPassingToBackend(session.id, passing);
saveState();
return;
}
entry.laps += 1;
entry.lastLapMs = lapMs;
entry.lastTimestamp = timestamp;
if (lapMs > 500 && (!entry.bestLapMs || lapMs < entry.bestLapMs)) {
entry.bestLapMs = lapMs;
}
result.passings.push(passing);
persistPassingToBackend(session.id, passing);
pushOverlayEvent("passing", `${entry.displayName || entry.driverName}${formatLap(entry.lastLapMs)}`);
const leaderboard = buildLeaderboard(session);
const leader = leaderboard[0];
if (leader?.key && lastOverlayLeaderKeyBySession[session.id] !== leader.key) {
lastOverlayLeaderKeyBySession[session.id] = leader.key;
pushOverlayEvent("leader", `${leader.displayName || leader.driverName} • P1`);
}
if (entry.bestLapMs && Number.isFinite(entry.bestLapMs)) {
const bestKey = `${session.id}:${entry.key}`;
const previousBest = lastOverlayBestLapByKey[bestKey];
if (!previousBest || entry.bestLapMs < previousBest) {
lastOverlayBestLapByKey[bestKey] = entry.bestLapMs;
pushOverlayEvent("bestlap", `${entry.displayName || entry.driverName}${formatLap(entry.bestLapMs)}`);
}
}
const top3Keys = leaderboard.slice(0, 3).map((row) => row.key);
const previousTop3 = lastOverlayTop3BySession[session.id] || [];
if (top3Keys.join("|") !== previousTop3.join("|")) {
lastOverlayTop3BySession[session.id] = top3Keys;
if (previousTop3.length) {
pushOverlayEvent("top3", `${t("overlay.mode_leaderboard")} • Top 3 updated`);
}
}
announcePassing(entry);
}