import React, { useState, useEffect, useRef } from 'react' import _ from 'lodash'; import { useParams, useNavigate } from 'react-router-dom' import { Button, Heading, Grid, View, useTheme, SliderField, Text } from '@aws-amplify/ui-react' import { Helmet } from 'react-helmet'; import { API, graphqlOperation, Auth } from 'aws-amplify' import { CONTROLLER_VISIBLE_STATES, DRIVING_RACE_STATES, MQTT_TOPICS, RACE_STATE_ICONS } from '../utils/constants'; import { PahoMqttClient, IotCoreMqttClient } from '../utils/mqttClient' import { getOrCreateUserId } from "../utils/localStorageFunctions" import { getPlayerByUUIDClaim } from '../utils/helpers' import awsExports from "../aws-exports"; import * as customStatements from '../graphql/custom-statements' const { REACT_APP_MQTT_ENDPOINT_HOST_REMOTE, REACT_APP_MQTT_ENDPOINT_HOST_LOCAL, REACT_APP_MQTT_ENDPOINT_PORT } = process.env export default function RaceController() { const { carId, raceId } = useParams() const { tokens } = useTheme(); const [throttle, setThrottle] = useState(0) const [laneSwitch, setRequestLaneSwitch] = useState(false) const [brakesOn, setRequestBrakes] = useState(false) const [client, setClient] = useState() const [race, setRace] = useState() const [player, setPlayer] = useState() const [subscriptions, setSubscriptions] = useState([]) const [allLaps, setAllLaps] = useState([]) const [latestLap, setLatestLap] = useState() const [fastestLap, setFastestLap] = useState() const [drivingState, setDrivingState] = useState(false) // The slider component doesn't update the label when setting the Throttle to 0 // We update this key value to 'force' a re-render when a race is stopped. const [throttleKeyForForceUpdate, setThrottleKeyForForceUpdate] = useState(Math.random()) const previousUpdateObject = useRef({}) const clientRef = useRef(client) // Required for the eventListener to get the updated client const throttleRef = useRef(throttle) const laneSwitchRef = useRef(laneSwitch) const requestBrakesRef = useRef(brakesOn) const playerRef = useRef(player) const raceRef = useRef(race) const subscriptionsRef = useRef(subscriptions) const navigate = useNavigate(); useEffect(() => { initWsClient() fetchCurrentRaceAndLapTimes() createSubscriptions() window.addEventListener('touchend', releaseButtons) return () => { unsubscribeSubscriptions() window.removeEventListener('touchend', releaseButtons) clientRef.current.disconnect() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const initWsClient = async () => { const isLocal = awsExports.aws_appsync_graphqlEndpoint.startsWith('http://') let client; if (isLocal === false) { console.log("Remote Client") const { accessKeyId, secretAccessKey, sessionToken } = await Auth.currentCredentials() client = new IotCoreMqttClient( REACT_APP_MQTT_ENDPOINT_HOST_REMOTE, accessKeyId, secretAccessKey, sessionToken, [MQTT_TOPICS.CAR_CONTROL_UPDATE] ) } else { console.log("Local Client") client = new PahoMqttClient( REACT_APP_MQTT_ENDPOINT_HOST_LOCAL, REACT_APP_MQTT_ENDPOINT_PORT, [MQTT_TOPICS.CAR_CONTROL_UPDATE] ) } clientRef.current = client setClient(client) } const unsubscribeSubscriptions = async () => { const subs = subscriptionsRef.current for (const sub of subs) { console.log("UNSUBSCRIBING: ", sub) sub.unsubscribe() } } const handleUnstartedRace = () => { const race = raceRef.current if (!CONTROLLER_VISIBLE_STATES.includes(race.currentRaceState)){ setTimeout(() => { navigate("/"); }, 2000); } } const createSubscriptions = async () => { // Clean up any pre-existing subscriptions unsubscribeSubscriptions() const updateRaceSubscription = await API.graphql({ query: customStatements.customOnUpdateRaceById, variables: { id: raceId } }).subscribe({ next: ({ _, value }) => { fetchCurrentRaceAndLapTimes() }, error: error => console.warn(error) }); const updateOverviewSubscription = await API.graphql({ query: customStatements.customOnUpdateOverview }).subscribe({ next: ({ _, value }) => { console.log("Updated Overview: ", value); }, error: error => console.warn(error) }); const lapTimeSubscription = await API.graphql({ query: customStatements.customOnCreateLapTime }).subscribe({ next: ({ _, value }) => { console.log("New LapTime: ", value); fetchLapTimes() }, error: error => console.warn(error) }); const subs = [ updateRaceSubscription, updateOverviewSubscription, lapTimeSubscription ] setSubscriptions(subs) subscriptionsRef.current = subs } const fetchCurrentRaceAndLapTimes = async () => { try { const race = await API.graphql(graphqlOperation(customStatements.customGetRace, { id: raceId })) const playerUUID = getOrCreateUserId() const player = getPlayerByUUIDClaim(race.data.getRace.players.items, playerUUID) playerRef.current = player setPlayer(player) setRace(race.data.getRace) const drivingState = DRIVING_RACE_STATES.includes(race.data.getRace.currentRaceState) if (!drivingState) { updateThrottle(0) setThrottleKeyForForceUpdate(Math.random()) } setDrivingState(drivingState) raceRef.current = race.data.getRace handleUnstartedRace() await fetchLapTimes() } catch (err) { console.error(err) } } const fetchLapTimes = async () => { const lapTimes = await API.graphql(graphqlOperation(customStatements.customLapTimesByPlayerId, { playerId: playerRef.current.id, limit: 1000 })) const lapTimeItems = lapTimes.data.lapTimesByPlayerId.items if (lapTimeItems.length > 0) { const fastestLap = lapTimeItems.reduce(function (prev, current) { return (prev.timeInMilliSec < current.timeInMilliSec) ? prev : current }) const latestLap = lapTimeItems.reduce(function (prev, current) { return (prev.createdAt > current.createdAt) ? prev : current }) setAllLaps(lapTimeItems) setFastestLap(fastestLap) setLatestLap(latestLap) } } const releaseButtons = () => { var updateObject = getUpdateBaseObject() updateObject.laneChangeReq = false updateObject.brakesOnReq = false laneSwitchRef.current = false requestBrakesRef.current = false sendCarUpdateObjectIfUpdated(updateObject) setRequestBrakes(false) setRequestLaneSwitch(false) } const getUpdateBaseObject = () => { // Base object initialiser return { raceId, carId, playerId: playerRef.current.id, throttle: throttleRef.current, laneChangeReq: laneSwitchRef.current, brakesOnReq: requestBrakesRef.current } } const sendCarUpdateObjectIfUpdated = (updateObject) => { const client = clientRef.current if (!_.isEqual(updateObject, previousUpdateObject.current)) { client.sendPayload(JSON.stringify(updateObject), MQTT_TOPICS.CAR_CONTROL_UPDATE) previousUpdateObject.current = updateObject } } const updateThrottle = (value) => { const updateObject = getUpdateBaseObject() updateObject.throttle = value throttleRef.current = value sendCarUpdateObjectIfUpdated(updateObject) setThrottle(value) } const requestLaneSwitch = () => { const updateObject = getUpdateBaseObject() updateObject.laneChangeReq = true laneSwitchRef.current = true sendCarUpdateObjectIfUpdated(updateObject) setRequestLaneSwitch(true) } const requestBrakes = () => { const updateObject = getUpdateBaseObject() updateObject.brakesOnReq = true requestBrakesRef.current = true sendCarUpdateObjectIfUpdated(updateObject) setRequestBrakes(true) } const matchingCarId = player?.playerCarId === carId const controllerVisible = matchingCarId && CONTROLLER_VISIBLE_STATES.includes(race.currentRaceState) return ( <> Race controller { (!controllerVisible) && You've got the wrong car page or the race hasn't started yet. Redirecting back ... } { controllerVisible && Nickname: {player.claims.items[0].username} {player.car.color} car Laps: {allLaps.length}/{race.nrOfLaps} Latest lap: {latestLap && `${latestLap?.timeInMilliSec}ms`} Fastest lap: {fastestLap && `${fastestLap?.timeInMilliSec}ms`} Race info: State: {RACE_STATE_ICONS[race.currentRaceState]} requestLaneSwitch()} margin={tokens.space.small} > Switch lane requestBrakes()} margin={tokens.space.small} > Brake } > ) }
Nickname: {player.claims.items[0].username}
{player.car.color} car
State: {RACE_STATE_ICONS[race.currentRaceState]}