Add multi-decoder split and pit timing support
This commit is contained in:
@@ -6,6 +6,13 @@ function parseRtcTime(value) {
|
||||
return Number.isFinite(ts) ? ts : null;
|
||||
}
|
||||
|
||||
const DECODER_ROLES = {
|
||||
main: { urlKey: "wsUrl", connectedKey: "connected", messageKey: "lastMessageAt", errorKey: "lastError" },
|
||||
split: { urlKey: "splitWsUrl", connectedKey: "splitConnected", messageKey: "splitLastMessageAt", errorKey: "splitLastError" },
|
||||
pit_entry: { urlKey: "pitEntryWsUrl", connectedKey: "pitEntryConnected", messageKey: "pitEntryLastMessageAt", errorKey: "pitEntryLastError" },
|
||||
pit_exit: { urlKey: "pitExitWsUrl", connectedKey: "pitExitConnected", messageKey: "pitExitLastMessageAt", errorKey: "pitExitLastError" },
|
||||
};
|
||||
|
||||
function resolveCompetitor(session, transponder, deps) {
|
||||
const { state, t, findEventTeamForPassing } = deps;
|
||||
const sessionType = String(session?.type || "").toLowerCase();
|
||||
@@ -110,29 +117,49 @@ function resolveCompetitor(session, transponder, deps) {
|
||||
}
|
||||
|
||||
export function connectDecoderHelper(deps) {
|
||||
deps.disconnectDecoder({ silent: true });
|
||||
deps.state.decoder.lastError = "";
|
||||
deps.state.decoder.splitLastError = "";
|
||||
deps.state.decoder.pitEntryLastError = "";
|
||||
deps.state.decoder.pitExitLastError = "";
|
||||
deps.saveState();
|
||||
connectDecoderRole("main", deps);
|
||||
if (deps.state.settings.splitDecoderEnabled && deps.state.settings.splitWsUrl) {
|
||||
connectDecoderRole("split", deps);
|
||||
}
|
||||
if (deps.state.settings.pitDecoderEnabled && deps.state.settings.pitEntryWsUrl) {
|
||||
connectDecoderRole("pit_entry", deps);
|
||||
}
|
||||
if (deps.state.settings.pitDecoderEnabled && deps.state.settings.pitExitWsUrl) {
|
||||
connectDecoderRole("pit_exit", deps);
|
||||
}
|
||||
}
|
||||
|
||||
function connectDecoderRole(role, deps) {
|
||||
const {
|
||||
state, t, saveState, getWsClient, setWsClient, getReconnectTimer, setReconnectTimer,
|
||||
getCurrentView, renderView, updateConnectionBadge, processDecoderMessage, disconnectDecoder,
|
||||
getCurrentView, renderView, updateConnectionBadge, processDecoderMessage,
|
||||
} = deps;
|
||||
const roleConfig = DECODER_ROLES[role] || DECODER_ROLES.main;
|
||||
const { connectedKey, messageKey: lastMessageKey, errorKey: lastErrorKey } = roleConfig;
|
||||
const url = state.settings[roleConfig.urlKey];
|
||||
if (!url) return;
|
||||
|
||||
disconnectDecoder({ silent: true });
|
||||
state.decoder.lastError = "";
|
||||
state.decoder[lastErrorKey] = "";
|
||||
saveState();
|
||||
|
||||
const url = state.settings.wsUrl;
|
||||
try {
|
||||
setWsClient(new WebSocket(url));
|
||||
setWsClient(role, new WebSocket(url));
|
||||
} catch (error) {
|
||||
state.decoder.lastError = t("error.ws_invalid", { msg: error instanceof Error ? error.message : String(error) });
|
||||
state.decoder[lastErrorKey] = t("error.ws_invalid", { msg: error instanceof Error ? error.message : String(error) });
|
||||
saveState();
|
||||
renderView();
|
||||
return;
|
||||
}
|
||||
|
||||
const wsClient = getWsClient();
|
||||
const wsClient = getWsClient(role);
|
||||
wsClient.onopen = () => {
|
||||
state.decoder.connected = true;
|
||||
state.decoder.lastError = "";
|
||||
state.decoder[connectedKey] = true;
|
||||
state.decoder[lastErrorKey] = "";
|
||||
saveState();
|
||||
updateConnectionBadge();
|
||||
if (["timing", "settings", "overlay"].includes(getCurrentView())) {
|
||||
@@ -141,7 +168,7 @@ export function connectDecoderHelper(deps) {
|
||||
};
|
||||
|
||||
wsClient.onmessage = (event) => {
|
||||
state.decoder.lastMessageAt = Date.now();
|
||||
state.decoder[lastMessageKey] = Date.now();
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
@@ -151,9 +178,9 @@ export function connectDecoderHelper(deps) {
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
parsed.forEach(processDecoderMessage);
|
||||
parsed.forEach((message) => processDecoderMessage(message, role));
|
||||
} else {
|
||||
processDecoderMessage(parsed);
|
||||
processDecoderMessage(parsed, role);
|
||||
}
|
||||
|
||||
saveState();
|
||||
@@ -164,15 +191,18 @@ export function connectDecoderHelper(deps) {
|
||||
};
|
||||
|
||||
wsClient.onclose = () => {
|
||||
state.decoder.connected = false;
|
||||
state.decoder[connectedKey] = false;
|
||||
saveState();
|
||||
updateConnectionBadge();
|
||||
|
||||
if (state.settings.autoReconnect) {
|
||||
clearTimeout(getReconnectTimer());
|
||||
setReconnectTimer(setTimeout(() => {
|
||||
if (!state.decoder.connected) {
|
||||
connectDecoderHelper(deps);
|
||||
clearTimeout(getReconnectTimer(role));
|
||||
setReconnectTimer(role, setTimeout(() => {
|
||||
const roleEnabled = role === "main"
|
||||
|| (role === "split" && state.settings.splitDecoderEnabled)
|
||||
|| (["pit_entry", "pit_exit"].includes(role) && state.settings.pitDecoderEnabled);
|
||||
if (!state.decoder[connectedKey] && roleEnabled) {
|
||||
connectDecoderRole(role, deps);
|
||||
}
|
||||
}, 2000));
|
||||
}
|
||||
@@ -183,7 +213,7 @@ export function connectDecoderHelper(deps) {
|
||||
};
|
||||
|
||||
wsClient.onerror = () => {
|
||||
state.decoder.lastError = t("error.decoder_connection");
|
||||
state.decoder[lastErrorKey] = t("error.decoder_connection");
|
||||
saveState();
|
||||
updateConnectionBadge();
|
||||
if (["timing", "settings", "overlay"].includes(getCurrentView())) {
|
||||
@@ -194,24 +224,29 @@ export function connectDecoderHelper(deps) {
|
||||
|
||||
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);
|
||||
}
|
||||
Object.keys(DECODER_ROLES).forEach((role) => {
|
||||
clearTimeout(getReconnectTimer(role));
|
||||
const wsClient = getWsClient(role);
|
||||
if (wsClient) {
|
||||
wsClient.onopen = null;
|
||||
wsClient.onmessage = null;
|
||||
wsClient.onclose = null;
|
||||
wsClient.onerror = null;
|
||||
wsClient.close();
|
||||
setWsClient(role, null);
|
||||
}
|
||||
});
|
||||
state.decoder.connected = false;
|
||||
state.decoder.splitConnected = false;
|
||||
state.decoder.pitEntryConnected = false;
|
||||
state.decoder.pitExitConnected = false;
|
||||
if (!options.silent) {
|
||||
saveState();
|
||||
}
|
||||
updateConnectionBadge();
|
||||
}
|
||||
|
||||
export function processDecoderMessageHelper(msg, deps) {
|
||||
export function processDecoderMessageHelper(msg, deps, decoderRole = "main") {
|
||||
const {
|
||||
state, t, getActiveSession, ensureSessionResult, normalizeStartMode, getSessionLapWindow,
|
||||
findEventTeamForPassing, persistPassingToBackend, pushOverlayEvent, buildLeaderboard,
|
||||
@@ -280,6 +315,68 @@ export function processDecoderMessageHelper(msg, deps) {
|
||||
entry.transponder = transponder;
|
||||
const startMode = normalizeStartMode(session.startMode);
|
||||
|
||||
if (decoderRole === "pit_entry") {
|
||||
if (entry.pitActive) return;
|
||||
entry.pitEnteredAt = timestamp;
|
||||
entry.pitActive = true;
|
||||
if (!Array.isArray(result.pitEvents)) result.pitEvents = [];
|
||||
result.pitEvents.push({ timestamp, type: "entry", transponder, competitorKey: key, driverId: entry.driverId, carId: entry.carId });
|
||||
pushOverlayEvent("pit", `${entry.displayName || entry.driverName} • PIT IN`);
|
||||
saveState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (decoderRole === "pit_exit") {
|
||||
const enteredAt = Number(entry.pitEnteredAt || 0);
|
||||
if (!enteredAt || timestamp <= enteredAt) return;
|
||||
const pitStopMs = timestamp - enteredAt;
|
||||
entry.pitActive = false;
|
||||
entry.pitEnteredAt = null;
|
||||
entry.pitStops = Math.max(0, Number(entry.pitStops || 0)) + 1;
|
||||
entry.lastPitStopMs = pitStopMs;
|
||||
entry.totalPitMs = Math.max(0, Number(entry.totalPitMs || 0)) + pitStopMs;
|
||||
if (!entry.bestPitStopMs || pitStopMs < entry.bestPitStopMs) entry.bestPitStopMs = pitStopMs;
|
||||
if (!Array.isArray(result.pitEvents)) result.pitEvents = [];
|
||||
if (!Array.isArray(result.pitStops)) result.pitStops = [];
|
||||
result.pitEvents.push({ timestamp, type: "exit", transponder, competitorKey: key, driverId: entry.driverId, carId: entry.carId, pitStopMs });
|
||||
result.pitStops.push({ enteredAt, exitedAt: timestamp, pitStopMs, transponder, competitorKey: key, driverId: entry.driverId, carId: entry.carId });
|
||||
pushOverlayEvent("pit", `${entry.displayName || entry.driverName} • PIT ${formatLap(pitStopMs)}`);
|
||||
saveState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (decoderRole === "split") {
|
||||
const baseTimestamp = Number(entry.lastTimestamp || (startMode === "mass" ? session.startedAt : 0) || 0);
|
||||
if (!baseTimestamp || timestamp <= baseTimestamp) return;
|
||||
if (entry.lastSplitTimestamp && timestamp - Number(entry.lastSplitTimestamp) < 1000) return;
|
||||
const splitMs = timestamp - baseTimestamp;
|
||||
const previousSplitMs = Number(entry.lastSplitMs || 0) || 0;
|
||||
entry.previousSplitMs = previousSplitMs || null;
|
||||
entry.lastSplitMs = splitMs;
|
||||
entry.lastSplitDeltaMs = previousSplitMs ? splitMs - previousSplitMs : null;
|
||||
entry.lastSplitTimestamp = timestamp;
|
||||
entry.lastSplitLap = Math.max(1, Number(entry.laps || 0) + 1);
|
||||
if (!entry.bestSplitMs || splitMs < entry.bestSplitMs) entry.bestSplitMs = splitMs;
|
||||
if (!Array.isArray(result.splits)) result.splits = [];
|
||||
result.splits.push({
|
||||
timestamp,
|
||||
transponder,
|
||||
driverId: entry.driverId,
|
||||
driverName: entry.driverName,
|
||||
carId: entry.carId,
|
||||
carName: entry.carName,
|
||||
competitorKey: key,
|
||||
lap: entry.lastSplitLap,
|
||||
splitMs,
|
||||
splitDeltaMs: entry.lastSplitDeltaMs,
|
||||
loopId: String(msg.loop_id || ""),
|
||||
decoderRole: "split",
|
||||
});
|
||||
pushOverlayEvent("split", `${entry.displayName || entry.driverName} • ${formatLap(splitMs)}`);
|
||||
saveState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (startMode === "staggered" && !entry.startTimestamp) {
|
||||
entry.startTimestamp = timestamp;
|
||||
entry.lastTimestamp = timestamp;
|
||||
|
||||
Reference in New Issue
Block a user