Extract decoder runtime and remove dead timing helpers
This commit is contained in:
1173
src/app.js
1173
src/app.js
File diff suppressed because it is too large
Load Diff
367
src/decoder_runtime.js
Normal file
367
src/decoder_runtime.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user