Add multi-decoder split and pit timing support
This commit is contained in:
119
src/app.js
119
src/app.js
@@ -119,7 +119,7 @@ const getEventName = (eventId) => getEventNameHelper(state, eventId, { t });
|
||||
const renderAssignmentList = (eventId) => renderAssignmentListView(eventId, { state, t, escapeHtml, getSessionsForEvent, getSessionTypeLabel, saveState, renderAssignmentList });
|
||||
const renderSessionsTable = (sessions) => renderSessionsTableView(sessions, { t, renderTable, escapeHtml, getEventName, getSessionTypeLabel, getStatusLabel, getModeLabel });
|
||||
const createDefaultAmmcConfig = () => createDefaultAmmcConfigHelper();
|
||||
const getManagedWsUrl = () => getManagedWsUrlHelper({ ammc, getBackendUrl });
|
||||
const getManagedWsUrl = (role = "main") => getManagedWsUrlHelper({ ammc, getBackendUrl }, role);
|
||||
const loadAmmcConfigFromBackend = () => loadAmmcConfigFromBackendHelper({ ammc, t, getBackendUrl, createDefaultAmmcConfig });
|
||||
const saveAmmcConfigToBackend = (config) => saveAmmcConfigToBackendHelper(config, { ammc, t, getBackendUrl, createDefaultAmmcConfig });
|
||||
const refreshAmmcStatus = () => refreshAmmcStatusHelper({ ammc, t, getBackendUrl });
|
||||
@@ -154,10 +154,10 @@ const connectDecoder = () => connectDecoderHelper({
|
||||
state,
|
||||
t,
|
||||
saveState,
|
||||
getWsClient: () => wsClient,
|
||||
setWsClient: (value) => { wsClient = value; },
|
||||
getReconnectTimer: () => reconnectTimer,
|
||||
setReconnectTimer: (value) => { reconnectTimer = value; },
|
||||
getWsClient: (role) => wsClients[role] || null,
|
||||
setWsClient: (role, value) => { wsClients[role] = value; },
|
||||
getReconnectTimer: (role) => reconnectTimers[role] || null,
|
||||
setReconnectTimer: (role, value) => { reconnectTimers[role] = value; },
|
||||
getCurrentView: () => currentView,
|
||||
renderView,
|
||||
updateConnectionBadge,
|
||||
@@ -168,13 +168,13 @@ const connectDecoder = () => connectDecoderHelper({
|
||||
const disconnectDecoder = (options = {}) => disconnectDecoderHelper(options, {
|
||||
state,
|
||||
saveState,
|
||||
getWsClient: () => wsClient,
|
||||
setWsClient: (value) => { wsClient = value; },
|
||||
getReconnectTimer: () => reconnectTimer,
|
||||
setReconnectTimer: (value) => { reconnectTimer = value; },
|
||||
getWsClient: (role) => wsClients[role] || null,
|
||||
setWsClient: (role, value) => { wsClients[role] = value; },
|
||||
getReconnectTimer: (role) => reconnectTimers[role] || null,
|
||||
setReconnectTimer: (role, value) => { reconnectTimers[role] = value; },
|
||||
updateConnectionBadge,
|
||||
});
|
||||
const processDecoderMessage = (msg) => processDecoderMessageHelper(msg, {
|
||||
const processDecoderMessage = (msg, decoderRole = "main") => processDecoderMessageHelper(msg, {
|
||||
state,
|
||||
t,
|
||||
getActiveSession,
|
||||
@@ -191,7 +191,7 @@ const processDecoderMessage = (msg) => processDecoderMessageHelper(msg, {
|
||||
lastOverlayLeaderKeyBySession,
|
||||
lastOverlayTop3BySession,
|
||||
lastOverlayBestLapByKey,
|
||||
});
|
||||
}, decoderRole);
|
||||
const renderDashboard = () => renderDashboardView({ state, dom, t, backend, getActiveSession, getStatusLabel, getSessionTypeLabel, getEventName, getModeLabel, getBackendUrl, formatLap, renderSessionsTable, setCurrentView: (view) => { currentView = view; }, updateViewUrl, renderNav, renderView, connectDecoder, disconnectDecoder, openOverlayWindow, ensureAudioContext, playPassingBeep, playFinishSiren, escapeHtml });
|
||||
const renderClasses = () => renderClassesView({ state, dom, t, selectedClassEditId: () => selectedClassEditId, setSelectedClassEditId: (value) => { selectedClassEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell });
|
||||
const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter: () => driverBrandFilter, setDriverBrandFilter: (value) => { driverBrandFilter = value; }, selectedDriverEditId: () => selectedDriverEditId, setSelectedDriverEditId: (value) => { selectedDriverEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeDriver, getClassName });
|
||||
@@ -788,6 +788,11 @@ const TRANSLATIONS = {
|
||||
"timing.connected": "Ansluten",
|
||||
"timing.disconnected": "Frånkopplad",
|
||||
"timing.last_message": "Senaste meddelande",
|
||||
"timing.main_decoder": "Main start/mål",
|
||||
"timing.split_decoder": "Mellantid",
|
||||
"timing.pit_entry_decoder": "Depå in",
|
||||
"timing.pit_exit_decoder": "Depå ut",
|
||||
"timing.pit_active": "I DEPÅ",
|
||||
"timing.control": "Sessionkontroll",
|
||||
"timing.speaker_panel": "Speaker-panel",
|
||||
"timing.speaker_panel_hint": "Dessa växlar slår av/på cues direkt för pågående session och overlay utan att lämna Tidtagning.",
|
||||
@@ -874,6 +879,20 @@ const TRANSLATIONS = {
|
||||
"timing.seeding_mode": "Seedning",
|
||||
"timing.position_grid_hint": "Griden visar startordningen för positionsstart i aktiv session.",
|
||||
"settings.decoder": "Decoder",
|
||||
"settings.main_ws_url": "Main WebSocket-url",
|
||||
"settings.enable_split_decoder": "Aktivera mellantidsdecoder",
|
||||
"settings.split_ws_url": "Mellantid WebSocket-url",
|
||||
"settings.enable_pit_decoders": "Aktivera depådecoders",
|
||||
"settings.decoder_roles_hint": "Main räknar varv. Mellantid mäter från senaste main-passering. Depå in/ut mäter depåstopp. Varje fysisk decoder måste ha egen IP och varje AMMC-kanal egen WebSocket-port.",
|
||||
"settings.managed_channels_hint": "Backend startar en separat AMMC-process per aktiverad kanal. Standardportar: main 9000, mellantid 9001, depå in 9002 och depå ut 9003.",
|
||||
"settings.pit_entry_ws_url": "Depå in WebSocket-url",
|
||||
"settings.pit_exit_ws_url": "Depå ut WebSocket-url",
|
||||
"settings.split_decoder_host": "Mellantid decoder IP / host",
|
||||
"settings.split_ws_port": "Mellantid WebSocket-port",
|
||||
"settings.pit_entry_decoder_host": "Depå in decoder IP / host",
|
||||
"settings.pit_entry_ws_port": "Depå in WebSocket-port",
|
||||
"settings.pit_exit_decoder_host": "Depå ut decoder IP / host",
|
||||
"settings.pit_exit_ws_port": "Depå ut WebSocket-port",
|
||||
"settings.auto_reconnect": "Auto-återanslut",
|
||||
"settings.save": "Spara",
|
||||
"settings.connect_now": "Anslut nu",
|
||||
@@ -990,6 +1009,8 @@ const TRANSLATIONS = {
|
||||
"table.laps": "Varv",
|
||||
"table.last_lap": "Senaste varv",
|
||||
"table.best_lap": "Bästa varv",
|
||||
"table.split": "Mellantid",
|
||||
"table.pit": "Depå",
|
||||
"table.gap": "Gap",
|
||||
"table.event": "Event",
|
||||
"table.result": "Resultat",
|
||||
@@ -1188,6 +1209,15 @@ const TRANSLATIONS = {
|
||||
"overlay.active_member": "Aktiv förare/bil",
|
||||
"overlay.top_three": "Topp 3",
|
||||
"guide.host_title": "Hur Managed AMMC körs",
|
||||
"guide.decoder_roles_title": "Decoderroller, IP och portar",
|
||||
"guide.decoder_roles_1": "1. Main start/mål är obligatorisk och är den enda kanalen som får räkna varv. Exempel: decoder `192.168.1.10` -> AMMC-port `9000` -> `ws://server-ip:9000`.",
|
||||
"guide.decoder_roles_2": "2. Mellantid är valfri och registrerar tiden från senaste main-passering utan att öka varvantalet. Exempel: `192.168.1.11` -> `9001`.",
|
||||
"guide.decoder_roles_3": "3. Depåtid kräver två loopar: Depå in öppnar stoppet och Depå ut stänger det. Exempel: `192.168.1.12` -> `9002`, samt `192.168.1.13` -> `9003`.",
|
||||
"guide.decoder_roles_4": "4. Decoder-IP är den fysiska decoderns adress. WebSocket-porten tillhör AMMC-processen på backend-hosten. Blanda inte ihop dessa två adresser.",
|
||||
"guide.decoder_roles_5": "5. Varje fysisk decoder ska ha unik IP och varje aktiverad AMMC-process ska ha unik port. Main/split/depå får aldrig dela samma WebSocket-port.",
|
||||
"guide.decoder_roles_6": "6. I Inställningar aktiverar du endast de extrakanaler som finns fysiskt, sparar Managed AMMC och klickar Starta AMMC. Använd sedan serverns WS-url och Anslut decoder.",
|
||||
"guide.decoder_roles_7": "7. Öppna TCP 9000 och de valfria portarna 9001-9003 i backend-hostens brandvägg om webbläsaren kör på en annan maskin. Port 8081 används för webb/API.",
|
||||
"guide.decoder_roles_8": "8. Kontrollera i Tidtagning att varje aktiverad roll står som Ansluten. Om main är offline räknas inga varv; om en extrakanal är offline fortsätter vanlig varvräkning.",
|
||||
"guide.host_1": "1. AMMC körs alltid på samma maskin som backend körs på (`npm.cmd start` i Windows eller `npm start` i Linux).",
|
||||
"guide.host_2": "2. Om du bara surfar in från en laptop/webbläsare startas ingen process där. Webbläsaren styr bara backend via HTTP.",
|
||||
"guide.host_3": "3. Kör backend på Linux-servern -> Linux-binären används: `AMMC/linux_x86-64/ammc-amb`.",
|
||||
@@ -1767,6 +1797,11 @@ const TRANSLATIONS = {
|
||||
"timing.connected": "Connected",
|
||||
"timing.disconnected": "Disconnected",
|
||||
"timing.last_message": "Last message",
|
||||
"timing.main_decoder": "Main start/finish",
|
||||
"timing.split_decoder": "Split timing",
|
||||
"timing.pit_entry_decoder": "Pit entry",
|
||||
"timing.pit_exit_decoder": "Pit exit",
|
||||
"timing.pit_active": "IN PIT",
|
||||
"timing.control": "Session Control",
|
||||
"timing.speaker_panel": "Speaker panel",
|
||||
"timing.speaker_panel_hint": "These toggles enable or disable cues live for the current session and overlay without leaving Timing.",
|
||||
@@ -1853,6 +1888,20 @@ const TRANSLATIONS = {
|
||||
"timing.seeding_mode": "Seeding",
|
||||
"timing.position_grid_hint": "The grid shows the start order for position start in the active session.",
|
||||
"settings.decoder": "Decoder",
|
||||
"settings.main_ws_url": "Main WebSocket URL",
|
||||
"settings.enable_split_decoder": "Enable split decoder",
|
||||
"settings.split_ws_url": "Split WebSocket URL",
|
||||
"settings.enable_pit_decoders": "Enable pit decoders",
|
||||
"settings.decoder_roles_hint": "Main counts laps. Split measures from the latest main crossing. Pit entry/exit measures pit stops. Every physical decoder needs its own IP and every AMMC channel needs a unique WebSocket port.",
|
||||
"settings.managed_channels_hint": "The backend starts one AMMC process per enabled channel. Default ports: main 9000, split 9001, pit entry 9002, and pit exit 9003.",
|
||||
"settings.pit_entry_ws_url": "Pit entry WebSocket URL",
|
||||
"settings.pit_exit_ws_url": "Pit exit WebSocket URL",
|
||||
"settings.split_decoder_host": "Split decoder IP / host",
|
||||
"settings.split_ws_port": "Split WebSocket port",
|
||||
"settings.pit_entry_decoder_host": "Pit entry decoder IP / host",
|
||||
"settings.pit_entry_ws_port": "Pit entry WebSocket port",
|
||||
"settings.pit_exit_decoder_host": "Pit exit decoder IP / host",
|
||||
"settings.pit_exit_ws_port": "Pit exit WebSocket port",
|
||||
"settings.auto_reconnect": "Auto reconnect",
|
||||
"settings.save": "Save",
|
||||
"settings.connect_now": "Connect Now",
|
||||
@@ -1969,6 +2018,8 @@ const TRANSLATIONS = {
|
||||
"table.laps": "Laps",
|
||||
"table.last_lap": "Last Lap",
|
||||
"table.best_lap": "Best Lap",
|
||||
"table.split": "Split",
|
||||
"table.pit": "Pit",
|
||||
"table.gap": "Gap",
|
||||
"table.event": "Event",
|
||||
"table.result": "Result",
|
||||
@@ -2167,6 +2218,15 @@ const TRANSLATIONS = {
|
||||
"overlay.active_member": "Active driver/car",
|
||||
"overlay.top_three": "Top 3",
|
||||
"guide.host_title": "How Managed AMMC Runs",
|
||||
"guide.decoder_roles_title": "Decoder roles, IP addresses, and ports",
|
||||
"guide.decoder_roles_1": "1. Main start/finish is required and is the only channel allowed to count laps. Example: decoder `192.168.1.10` -> AMMC port `9000` -> `ws://server-ip:9000`.",
|
||||
"guide.decoder_roles_2": "2. Split timing is optional and records time from the latest main crossing without incrementing laps. Example: `192.168.1.11` -> `9001`.",
|
||||
"guide.decoder_roles_3": "3. Pit timing needs two loops: Pit entry opens the stop and Pit exit closes it. Example: `192.168.1.12` -> `9002`, and `192.168.1.13` -> `9003`.",
|
||||
"guide.decoder_roles_4": "4. Decoder IP is the physical decoder address. The WebSocket port belongs to the AMMC process on the backend host. Do not confuse these two addresses.",
|
||||
"guide.decoder_roles_5": "5. Every physical decoder needs a unique IP and every enabled AMMC process needs a unique port. Main/split/pit must never share a WebSocket port.",
|
||||
"guide.decoder_roles_6": "6. In Settings, enable only physically installed extra channels, save Managed AMMC, and click Start AMMC. Then use the server WS URLs and Connect decoder.",
|
||||
"guide.decoder_roles_7": "7. Allow TCP 9000 and optional ports 9001-9003 through the backend host firewall when the browser runs on another machine. Port 8081 serves the web UI/API.",
|
||||
"guide.decoder_roles_8": "8. Verify in Timing that every enabled role is Connected. If main is offline no laps are counted; an offline optional channel does not stop normal lap counting.",
|
||||
"guide.host_1": "1. AMMC always runs on the backend host (`npm.cmd start` on Windows or `npm start` on Linux).",
|
||||
"guide.host_2": "2. If you only browse from a laptop/browser, no process is started there. The browser only controls the backend over HTTP.",
|
||||
"guide.host_3": "3. Run the backend on Linux -> the Linux binary is used: `AMMC/linux_x86-64/ammc-amb`.",
|
||||
@@ -2222,8 +2282,8 @@ const publicOverlayMode = Boolean(publicOverlayMatch);
|
||||
const state = loadState();
|
||||
const requestedView = String(urlParams.get("view") || "").toLowerCase();
|
||||
let currentView = overlayMode ? "overlay" : NAV_ITEMS.some((item) => item.id === requestedView) ? requestedView : "dashboard";
|
||||
let wsClient = null;
|
||||
let reconnectTimer = null;
|
||||
let wsClients = { main: null, split: null, pit_entry: null, pit_exit: null };
|
||||
let reconnectTimers = { main: null, split: null, pit_entry: null, pit_exit: null };
|
||||
let backendSyncTimer = null;
|
||||
let appVersionPollTimer = null;
|
||||
let baselineAppVersion = "";
|
||||
@@ -2611,6 +2671,11 @@ function loadState() {
|
||||
activeSessionId: parsed.activeSessionId || null,
|
||||
settings: {
|
||||
wsUrl: parsed.settings?.wsUrl || "ws://127.0.0.1:9000",
|
||||
splitDecoderEnabled: parsed.settings?.splitDecoderEnabled === true,
|
||||
splitWsUrl: String(parsed.settings?.splitWsUrl || "ws://127.0.0.1:9001"),
|
||||
pitDecoderEnabled: parsed.settings?.pitDecoderEnabled === true,
|
||||
pitEntryWsUrl: String(parsed.settings?.pitEntryWsUrl || "ws://127.0.0.1:9002"),
|
||||
pitExitWsUrl: String(parsed.settings?.pitExitWsUrl || "ws://127.0.0.1:9003"),
|
||||
backendUrl: parsed.settings?.backendUrl || getDefaultBackendUrl(),
|
||||
language: parsed.settings?.language || DEFAULT_LANGUAGE,
|
||||
theme: AVAILABLE_THEMES.includes(String(parsed.settings?.theme || "").toLowerCase()) ? String(parsed.settings.theme).toLowerCase() : DEFAULT_THEME,
|
||||
@@ -2641,8 +2706,17 @@ function loadState() {
|
||||
},
|
||||
decoder: {
|
||||
connected: false,
|
||||
splitConnected: false,
|
||||
pitEntryConnected: false,
|
||||
pitExitConnected: false,
|
||||
lastMessageAt: null,
|
||||
splitLastMessageAt: null,
|
||||
pitEntryLastMessageAt: null,
|
||||
pitExitLastMessageAt: null,
|
||||
lastError: "",
|
||||
splitLastError: "",
|
||||
pitEntryLastError: "",
|
||||
pitExitLastError: "",
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
@@ -2661,6 +2735,11 @@ function loadState() {
|
||||
activeSessionId: null,
|
||||
settings: {
|
||||
wsUrl: "ws://127.0.0.1:9000",
|
||||
splitDecoderEnabled: false,
|
||||
splitWsUrl: "ws://127.0.0.1:9001",
|
||||
pitDecoderEnabled: false,
|
||||
pitEntryWsUrl: "ws://127.0.0.1:9002",
|
||||
pitExitWsUrl: "ws://127.0.0.1:9003",
|
||||
backendUrl: getDefaultBackendUrl(),
|
||||
language: DEFAULT_LANGUAGE,
|
||||
theme: DEFAULT_THEME,
|
||||
@@ -2689,8 +2768,17 @@ function loadState() {
|
||||
},
|
||||
decoder: {
|
||||
connected: false,
|
||||
splitConnected: false,
|
||||
pitEntryConnected: false,
|
||||
pitExitConnected: false,
|
||||
lastMessageAt: null,
|
||||
splitLastMessageAt: null,
|
||||
pitEntryLastMessageAt: null,
|
||||
pitExitLastMessageAt: null,
|
||||
lastError: "",
|
||||
splitLastError: "",
|
||||
pitEntryLastError: "",
|
||||
pitExitLastError: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3234,7 +3322,10 @@ function updateHeaderState() {
|
||||
|
||||
function updateConnectionBadge() {
|
||||
const isOnline = state.decoder.connected;
|
||||
dom.connectionBadge.textContent = isOnline ? t("ui.decoder_online") : t("ui.decoder_offline");
|
||||
const enabledExtraCount = Number(state.settings.splitDecoderEnabled) + (state.settings.pitDecoderEnabled ? 2 : 0);
|
||||
const connectedExtraCount = Number(state.decoder.splitConnected) + Number(state.decoder.pitEntryConnected) + Number(state.decoder.pitExitConnected);
|
||||
const extraStatus = enabledExtraCount ? ` • ${connectedExtraCount}/${enabledExtraCount}` : "";
|
||||
dom.connectionBadge.textContent = `${isOnline ? t("ui.decoder_online") : t("ui.decoder_offline")}${extraStatus}`;
|
||||
dom.connectionBadge.className = `badge ${isOnline ? "badge-online" : "badge-offline"}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user