diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx index d6e67f59..c1de03fc 100644 --- a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx +++ b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtButtons.tsx @@ -1,6 +1,5 @@ import { CSSProperties, useRef } from "react"; import { useButtons } from "./useButtons"; -import { useSounds } from "./useSounds"; const MRT_BUTTON_STYLES: CSSProperties = { cursor: "pointer", @@ -45,6 +44,34 @@ export const MrtButtons = () => { return ( <> + {/* BELOW DISPLAY */} + + + + + + + {/* LINE SELECT KEY */} { onClick={handleKlick("3l")} style={{ gridArea: "9 / 2 / 11 / 3", ...MRT_BUTTON_STYLES }} /> + {/* NUM PAD */} { onClick={handleKlick("0")} style={{ gridArea: "11 / 16 / 13 / 17", ...MRT_BUTTON_STYLES }} /> + ); }; diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.tsx b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.tsx index 9a7b23f8..6f3f0197 100644 --- a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.tsx +++ b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtDisplay.tsx @@ -174,7 +174,7 @@ export const MrtDisplay = () => { const DisplayText = ({ pageName }: { pageName: SetPageParams["page"] }) => { return (
{

@@ -203,7 +203,7 @@ export const MrtDisplay = () => { )} {!connectedAircraft && <>Keine Verbindung}

-

+

{room?.name || "Keine RG gefunden"}

@@ -213,7 +213,7 @@ export const MrtDisplay = () => { )} {pageName == "voice-call" && (

-

+

{stringifiedData.callTextHeader}

@@ -240,15 +240,25 @@ export const MrtDisplay = () => { alt="" width={1000} height={1000} - className={`z-10 col-span-full row-span-full`} + className={cn(popup && "brightness-75 filter", "z-10 col-span-full row-span-full")} /> {nextImage && ( -

+
)} -
+
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx index 2a7c5e70..1e1dca31 100644 --- a/apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx +++ b/apps/dispatch/app/(app)/pilot/_components/mrt/MrtPopups.tsx @@ -2,20 +2,27 @@ import { SetPopupParams, useMrtStore } from "_store/pilot/MrtStore"; import Image, { StaticImageData } from "next/image"; import { useEffect, useState } from "react"; import IAMGE_POPUP_LOGIN from "./images/POPUP_login.png"; +import GROUP_SELECTION_POPUP_LOGIN from "./images/POPUP_group_selection.png"; import IAMGE_POPUP_SDS_RECEIVED from "./images/POPUP_SDS_incomming.png"; import IAMGE_POPUP_SDS_SENT from "./images/POPUP_SDS_sent.png"; import IAMGE_POPUP_STATUS_SENT from "./images/POPUP_Status_sent.png"; +import { ROOMS } from "_data/livekitRooms"; import { cn, useDebounce } from "@repo/shared-components"; import { usePilotConnectionStore } from "_store/pilot/connectionStore"; import { fmsStatusDescription, fmsStatusDescriptionShort } from "_data/fmsStatusDescription"; import { pilotSocket } from "(app)/pilot/socket"; import { StationStatus } from "@repo/db"; import { useSounds } from "./useSounds"; +import { useButtons } from "./useButtons"; +import { useAudioStore } from "_store/audioStore"; export const MrtPopups = () => { const { sdsReceivedSoundRef } = useSounds(); - - const { popup, setPopup, setStringifiedData, stringifiedData } = useMrtStore((state) => state); + const { handleKlick } = useButtons(); + const { selectedRoom } = useAudioStore(); + const { popup, page, setPopup, setStringifiedData, stringifiedData } = useMrtStore( + (state) => state, + ); const { connectedAircraft, status } = usePilotConnectionStore((state) => state); const [popupImage, setPopupImage] = useState(null); @@ -33,6 +40,9 @@ export const MrtPopups = () => { case "login": setPopupImage(IAMGE_POPUP_LOGIN); break; + case "group-selection": + setPopupImage(GROUP_SELECTION_POPUP_LOGIN); + break; case undefined: case null: setPopupImage(null); @@ -44,17 +54,32 @@ export const MrtPopups = () => { () => { if (popup == "login") return; if (popup == "sds-received") return; + if (popup == "group-selection") return; setPopup(null); }, 3000, [popup], ); + useDebounce( + () => { + if (popup == "group-selection") { + if (selectedRoom?.id === stringifiedData.groupSelectionGroupId) { + setPopup(null); + } else { + handleKlick("3l")(); + } + } + }, + 5000, + [page, stringifiedData.groupSelectionGroupId, selectedRoom], + ); + useEffect(() => { - if (status === "connecting") { + if (status === "connecting" && page !== "off" && page !== "startup") { setPopup({ popup: "login" }); } - }, [status, setPopup]); + }, [status, setPopup, page]); useDebounce( () => { @@ -80,9 +105,11 @@ export const MrtPopups = () => { if (!popupImage || !popup) return null; const DisplayText = ({ pageName }: { pageName: SetPopupParams["popup"] }) => { + const group = ROOMS.find((r) => r.id === stringifiedData.groupSelectionGroupId); + return (
{ {stringifiedData.sdsText}

)} + {pageName == "group-selection" && ( + <> +

{group?.name}

+

+ {stringifiedData.groupSelectionGroupId} +

+ + )}
); }; return ( <> - +
diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_group_selection.png b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_group_selection.png new file mode 100644 index 00000000..72834534 Binary files /dev/null and b/apps/dispatch/app/(app)/pilot/_components/mrt/images/POPUP_group_selection.png differ diff --git a/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts b/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts index 35d619a5..04fa70a6 100644 --- a/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts +++ b/apps/dispatch/app/(app)/pilot/_components/mrt/useButtons.ts @@ -8,12 +8,42 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useSounds } from "./useSounds"; import { sendSdsStatusMessageAPI } from "_querys/missions"; import { useSession } from "next-auth/react"; +import { ROOMS } from "_data/livekitRooms"; +import { useAudioStore } from "_store/audioStore"; + +type ButtonTypes = + | "1" + | "2" + | "3" + | "4" + | "5" + | "6" + | "7" + | "8" + | "9" + | "0" + | "home" + | "3l" + | "3r" + | "wheel-knob" + | "arrow-up" + | "arrow-down" + | "arrow-left" + | "arrow-right" + | "end-call"; export const useButtons = () => { const session = useSession(); + const { connect, setSelectedRoom, selectedRoom } = useAudioStore((state) => state); + const { longBtnPressSoundRef, statusSentSoundRef } = useSounds(); const queryClient = useQueryClient(); - const { selectedStation: station, connectedAircraft } = usePilotConnectionStore((state) => state); + const { + status: pilotState, + selectedStation, + connectedAircraft, + } = usePilotConnectionStore((state) => state); + const sendSdsStatusMutation = useMutation({ mutationFn: async ({ sdsMessage }: { sdsMessage: MissionSdsStatusLog }) => { if (!connectedAircraft?.id) throw new Error("No connected aircraft"); @@ -34,82 +64,161 @@ export const useButtons = () => { }) => editConnectedAircraftAPI(aircraftId, data), }); - const { setPage, setPopup, page, popup, setStringifiedData } = useMrtStore((state) => state); + const { setPage, setPopup, page, popup, setStringifiedData, stringifiedData } = useMrtStore( + (state) => state, + ); - const handleHold = - (button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "home" | "3l" | "3r") => - async () => { - /* if (connectionStatus !== "connected") return; */ - if (button === "1" && page === "off") { - setPage({ page: "startup" }); - return; - } - if (!station) return; - if (!session.data?.user) return; - if (!connectedAircraft?.id) return; - if ( - button === "1" || - button === "2" || - button === "3" || - button === "4" || - button === "6" || - button === "7" || - button === "8" - ) { - longBtnPressSoundRef.current?.play(); - const delay = Math.random() * 1500 + 500; - setTimeout(async () => { - await updateAircraftMutation.mutateAsync({ - aircraftId: connectedAircraft.id, + const role = + (pilotState == "connected" && selectedStation?.bosCallsignShort) || + session.data?.user?.publicId; + + const handleHold = (button: ButtonTypes) => async () => { + /* if (connectionStatus !== "connected") return; */ + if (button === "end-call") { + setPage({ page: "off" }); + setPopup(null); + } + if (button === "1" && page === "off") { + setPage({ page: "startup" }); + return; + } + if (!selectedStation) return; + if (!session.data?.user) return; + if (!connectedAircraft?.id) return; + if ( + button === "1" || + button === "2" || + button === "3" || + button === "4" || + button === "6" || + button === "7" || + button === "8" + ) { + longBtnPressSoundRef.current?.play(); + const delay = Math.random() * 1500 + 500; + setTimeout(async () => { + await updateAircraftMutation.mutateAsync({ + aircraftId: connectedAircraft.id, + data: { + fmsStatus: button, + }, + }); + setPopup({ popup: "status-sent" }); + statusSentSoundRef.current?.play(); + }, delay); + } else if (button === "5" || button === "9" || button === "0") { + longBtnPressSoundRef.current?.play(); + const delay = Math.random() * 1500 + 500; + setTimeout(async () => { + await sendSdsStatusMutation.mutateAsync({ + sdsMessage: { + type: "sds-status-log", + auto: false, + timeStamp: new Date().toISOString(), data: { - fmsStatus: button, + direction: "to-lst", + stationId: selectedStation.id, + station: selectedStation, + user: getPublicUser(session.data?.user), + status: button, }, - }); - setPopup({ popup: "status-sent" }); - statusSentSoundRef.current?.play(); - }, delay); - } else if (button === "5" || button === "9" || button === "0") { - longBtnPressSoundRef.current?.play(); - const delay = Math.random() * 1500 + 500; - setTimeout(async () => { - await sendSdsStatusMutation.mutateAsync({ - sdsMessage: { - type: "sds-status-log", - auto: false, - timeStamp: new Date().toISOString(), - data: { - direction: "to-lst", - stationId: station.id, - station, - user: getPublicUser(session.data?.user), - status: button, - }, - }, - }); - setStringifiedData({ sentSdsText: button }); - statusSentSoundRef.current?.play(); - setPopup({ popup: "sds-sent" }); - }, delay); - } - }; + }, + }); + setStringifiedData({ sentSdsText: button }); + statusSentSoundRef.current?.play(); + setPopup({ popup: "sds-sent" }); + }, delay); + } + }; - const handleKlick = - (button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "home" | "3l" | "3r") => - async () => { - //implement Kurzwahl when button is clicked short to dial - if (popup == "sds-received" && button === "3r") { - setPopup(null); - } - return false; - }; + const handleKlick = (button: ButtonTypes) => async () => { + console.log("Button clicked:", button); + //implement Kurzwahl when button is clicked short to dial + + switch (button) { + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + //handle short press number buttons for kurzwahl + if (popup === "group-selection") { + if (stringifiedData.groupSelectionGroupId?.length === 4) { + setStringifiedData({ groupSelectionGroupId: button }); + } else { + setStringifiedData({ + groupSelectionGroupId: (stringifiedData.groupSelectionGroupId || "") + button, + }); + } + } + if (page === "home" && !popup) { + setPopup({ popup: "group-selection" }); + setStringifiedData({ groupSelectionGroupId: button }); + } + break; + case "3r": + if (popup === "sds-received" || popup === "group-selection") { + setPopup(null); + } else if (page === "home") { + setPopup({ popup: "group-selection" }); + setStringifiedData({ groupSelectionGroupId: selectedRoom?.id || ROOMS[0]!.id }); + } else if (page === "voice-call") { + setPage({ page: "home" }); + } + break; + case "wheel-knob": + setPopup(popup === "group-selection" ? null : { popup: "group-selection" }); + setStringifiedData({ groupSelectionGroupId: selectedRoom?.id || ROOMS[0]!.id }); + break; + case "arrow-right": + if (popup === "group-selection") { + let currentGroupIndex = ROOMS.findIndex( + (r) => r.id === stringifiedData.groupSelectionGroupId, + ); + if (currentGroupIndex === ROOMS.length - 1) currentGroupIndex = -1; + const nextGroup = ROOMS[currentGroupIndex + 1]; + if (nextGroup) { + setStringifiedData({ groupSelectionGroupId: nextGroup.id }); + } + } + break; + case "arrow-left": + if (popup === "group-selection") { + let currentGroupIndex = ROOMS.findIndex( + (r) => r.id === stringifiedData.groupSelectionGroupId, + ); + if (currentGroupIndex === 0) currentGroupIndex = ROOMS.length; + const previousGroup = ROOMS[currentGroupIndex - 1]; + if (previousGroup) { + setStringifiedData({ groupSelectionGroupId: previousGroup.id }); + } + } + break; + case "3l": + if (popup === "group-selection") { + const group = ROOMS.find((r) => r.id === stringifiedData.groupSelectionGroupId); + if (group && role) { + setSelectedRoom(group); + connect(group, role); + setPopup(null); + } + } + } + return false; + }; useEffect(() => { pilotSocket.on("connect", () => { const { page } = useMrtStore.getState(); - if (!station || page !== "off") return; + if (!selectedStation || page !== "off") return; setPage({ page: "startup" }); }); - }, [setPage, station, setPopup]); + }, [setPage, selectedStation, setPopup]); return { handleKlick, handleHold }; }; diff --git a/apps/dispatch/app/(app)/pilot/page.tsx b/apps/dispatch/app/(app)/pilot/page.tsx index b5874665..34686bf2 100644 --- a/apps/dispatch/app/(app)/pilot/page.tsx +++ b/apps/dispatch/app/(app)/pilot/page.tsx @@ -97,7 +97,17 @@ const PilotPage = () => {
-

MRT & DME

+
+

MRT & DME

+ + Hilfe + +
{ const { + selectedRoom, speakingParticipants, resetSpeakingParticipants, isTalking, @@ -37,8 +38,8 @@ export const Audio = () => { room, message, removeMessage, + setSelectedRoom, } = useAudioStore(); - const [selectedRoom, setSelectedRoom] = useState("VAR_LST_RD_01"); useSounds({ isReceiving: speakingParticipants.length > 0, @@ -48,7 +49,7 @@ export const Audio = () => { }); const { selectedStation, status: pilotState } = usePilotConnectionStore((state) => state); - const { selectedZone, status: dispatcherState } = useDispatchConnectionStore((state) => state); + const { status: dispatcherState } = useDispatchConnectionStore((state) => state); const session = useSession(); const [isReceivingBlick, setIsReceivingBlick] = useState(false); const [recentSpeakers, setRecentSpeakers] = useState([]); @@ -93,7 +94,7 @@ export const Audio = () => { const canStopOtherSpeakers = dispatcherState === "connected"; const role = - (dispatcherState === "connected" && selectedZone) || + (dispatcherState === "connected" && "VAR LST") || (pilotState == "connected" && selectedStation?.bosCallsignShort) || session.data?.user?.publicId; @@ -190,9 +191,9 @@ export const Audio = () => { className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left" onClick={() => { if (!role) return; - if (selectedRoom === r.name) return; - setSelectedRoom(r.name); - connect(r.name, role); + if (selectedRoom?.name === r.name) return; + setSelectedRoom(r); + connect(r, role); }} > {room?.name === r.name && ( diff --git a/apps/dispatch/app/_data/livekitRooms.ts b/apps/dispatch/app/_data/livekitRooms.ts index 0ed6c0b5..f98a61aa 100644 --- a/apps/dispatch/app/_data/livekitRooms.ts +++ b/apps/dispatch/app/_data/livekitRooms.ts @@ -1,7 +1,7 @@ export const ROOMS = [ - { name: "VAR_LST_RD_01", id: 2201 }, - { name: "VAR_LST_RD_02", id: 2202 }, - { name: "VAR_LST_RD_03", id: 2203 }, - { name: "VAR_LST_RD_04", id: 2204 }, - { name: "VAR_LST_RD_05", id: 2205 }, + { name: "VAR_LST_RD_01", id: "2201" }, + { name: "VAR_LST_RD_02", id: "2202" }, + { name: "VAR_LST_RD_03", id: "2203" }, + { name: "VAR_LST_RD_04", id: "2204" }, + { name: "VAR_LST_RD_05", id: "2205" }, ]; diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index 7ce25c64..714264b2 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -21,12 +21,15 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { changeDispatcherAPI } from "_querys/dispatcher"; import { getRadioStream } from "_helpers/radioEffect"; import { usePilotConnectionStore } from "_store/pilot/connectionStore"; +import { ROOMS } from "_data/livekitRooms"; let interval: NodeJS.Timeout; +const connectedSound = new Audio("/sounds/403.wav"); + type TalkState = { addSpeakingParticipant: (participant: Participant) => void; - connect: (roomName: string, role: string) => void; + connect: (room: (typeof ROOMS)[number] | undefined, role: string) => void; connectionQuality: ConnectionQuality; disconnect: () => void; isTalking: boolean; @@ -44,6 +47,8 @@ type TalkState = { radioVolume: number; dmeVolume: number; }; + selectedRoom?: (typeof ROOMS)[number]; + setSelectedRoom: (room: (typeof ROOMS)[number]) => void; speakingParticipants: Participant[]; state: "connecting" | "connected" | "disconnected" | "error"; toggleTalking: () => void; @@ -72,6 +77,10 @@ export const useAudioStore = create((set, get) => ({ remoteParticipants: 0, connectionQuality: ConnectionQuality.Unknown, room: null, + selectedRoom: ROOMS[0], + setSelectedRoom: (room) => { + set({ selectedRoom: room }); + }, resetSpeakingParticipants: (source: string) => { set({ speakingParticipants: [], @@ -117,11 +126,11 @@ export const useAudioStore = create((set, get) => ({ (oldSettings.micDeviceId !== newSettings.micDeviceId || oldSettings.micVolume !== newSettings.micVolume) ) { - const { room, disconnect, connect } = get(); + const { room, disconnect, connect, selectedRoom } = get(); const role = room?.localParticipant.attributes.role; - if (room?.name || role) { + if (selectedRoom || role) { disconnect(); - connect(room?.name || "", role || "user"); + connect(selectedRoom, role || "user"); } } }, @@ -160,7 +169,7 @@ export const useAudioStore = create((set, get) => ({ set((state) => ({ isTalking: !state.isTalking, transmitBlocked: false })); }, - connect: async (roomName, role) => { + connect: async (_room, role) => { set({ state: "connecting" }); try { @@ -172,10 +181,12 @@ export const useAudioStore = create((set, get) => ({ connectedRoom.removeAllListeners(); } + const { selectedRoom } = get(); + const url = process.env.NEXT_PUBLIC_LIVEKIT_URL; if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set"); - const token = await getToken(roomName); + const token = await getToken(_room?.name || selectedRoom?.name || "VAR_LST_RD_01"); if (!token) throw new Error("Fehlende Berechtigung"); const room = new Room({}); await room.prepareConnection(url, token); @@ -186,7 +197,7 @@ export const useAudioStore = create((set, get) => ({ if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) { changeDispatcherAPI(dispatchState.connectedDispatcher?.id, { - zone: roomName, + zone: _room?.name || selectedRoom?.name || "VAR_LST_RD_01", ghostMode: dispatchState.ghostMode, }); } @@ -208,7 +219,7 @@ export const useAudioStore = create((set, get) => ({ source: Track.Source.Microphone, }); await publishedTrack.mute(); - + connectedSound.play().catch((e) => console.error("Fehler beim Abspielen des Sounds", e)); set({ localRadioTrack: publishedTrack }); set({ state: "connected", room, isTalking: false, message: null }); diff --git a/apps/dispatch/app/_store/dispatch/connectionStore.ts b/apps/dispatch/app/_store/dispatch/connectionStore.ts index 2e5bb4c1..9663e473 100644 --- a/apps/dispatch/app/_store/dispatch/connectionStore.ts +++ b/apps/dispatch/app/_store/dispatch/connectionStore.ts @@ -48,7 +48,7 @@ export const useDispatchConnectionStore = create((set) => ({ dispatchSocket.on("connect", () => { const { logoffTime, selectedZone, ghostMode } = useDispatchConnectionStore.getState(); - useAudioStore.getState().connect("VAR_LST_RD_01", selectedZone || "Leitstelle"); + useAudioStore.getState().connect(undefined, selectedZone || "Leitstelle"); dispatchSocket.emit("connect-dispatch", { logoffTime, selectedZone, diff --git a/apps/dispatch/app/_store/pilot/MrtStore.ts b/apps/dispatch/app/_store/pilot/MrtStore.ts index a2a4525f..f8432402 100644 --- a/apps/dispatch/app/_store/pilot/MrtStore.ts +++ b/apps/dispatch/app/_store/pilot/MrtStore.ts @@ -19,6 +19,10 @@ interface SetSdsReceivedPopupParams { popup: "sds-received"; } +interface SetGroupSelectionPopupParams { + popup: "group-selection"; +} + interface SetStatusSentPopupParams { popup: "status-sent"; } @@ -40,6 +44,7 @@ export type SetPageParams = export type SetPopupParams = | SetStatusSentPopupParams | SetSdsSentPopupParams + | SetGroupSelectionPopupParams | SetSdsReceivedPopupParams | SetLoginPopupParams; @@ -47,6 +52,7 @@ interface StringifiedData { sdsText?: string; sentSdsText?: string; + groupSelectionGroupId?: string; callTextHeader?: string; } @@ -69,7 +75,9 @@ interface MrtStore { export const useMrtStore = create((set) => ({ page: "off", nightMode: false, - stringifiedData: {}, + stringifiedData: { + groupSelectionGroupId: "2201", + }, setNightMode: (nightMode) => set({ nightMode }), setStringifiedData: (data) => set((state) => ({ diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts index 15e3109d..1ca44645 100644 --- a/apps/dispatch/app/_store/pilot/connectionStore.ts +++ b/apps/dispatch/app/_store/pilot/connectionStore.ts @@ -86,7 +86,7 @@ pilotSocket.on("connect", () => { usePilotConnectionStore.setState({ status: "connected", message: "" }); const { logoffTime, selectedStation, debug } = usePilotConnectionStore.getState(); dispatchSocket.disconnect(); - useAudioStore.getState().connect("VAR_LST_RD_01", selectedStation?.bosCallsignShort || "pilot"); + useAudioStore.getState().connect(undefined, selectedStation?.bosCallsignShort || "pilot"); pilotSocket.emit("connect-pilot", { logoffTime,