Skip to content

Commit

Permalink
general: Add vehicle discovery system (electron-only)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl committed Nov 28, 2024
1 parent cfe761a commit 519bbb8
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 2 deletions.
4 changes: 4 additions & 0 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { app, BrowserWindow, protocol, screen } from 'electron'
import { join } from 'path'

import { setupNetworkService } from './services/network'

export const ROOT_PATH = {
dist: join(__dirname, '..'),
}
Expand Down Expand Up @@ -59,6 +61,8 @@ protocol.registerSchemesAsPrivileged([
},
])

setupNetworkService()

app.whenReady().then(createWindow)

app.on('before-quit', () => {
Expand Down
5 changes: 5 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
getNetworkInfo: () => ipcRenderer.invoke('get-network-info'),
})
39 changes: 39 additions & 0 deletions electron/services/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ipcMain } from 'electron'
import { networkInterfaces } from 'os'

/**
* Information about the network
*/
interface NetworkInfo {
/**
* The subnet of the local machine
*/
subnet: string
}

/**
* Get the network information
* @returns {NetworkInfo} The network information
*/
const getNetworkInfo = (): NetworkInfo => {
const nets = networkInterfaces()

for (const name of Object.keys(nets)) {
for (const net of nets[name] ?? []) {
// Skip over non-IPv4 and internal addresses
if (net.family === 'IPv4' && !net.internal) {
// Return the subnet (e.g., if IP is 192.168.1.5, return 192.168.1)
return { subnet: net.address.split('.').slice(0, 3).join('.') }
}
}
}

throw new Error('No network interface found.')
}

/**
* Setup the network service
*/
export const setupNetworkService = (): void => {
ipcMain.handle('get-network-info', getNetworkInfo)
}
18 changes: 17 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -315,22 +315,25 @@
<About v-if="showAboutDialog" @update:show-about-dialog="showAboutDialog = $event" />
<Tutorial :show-tutorial="interfaceStore.isTutorialVisible" />
<VideoLibraryModal :open-modal="interfaceStore.isVideoLibraryVisible" />
<VehicleDiscoveryDialog v-model="showDiscoveryDialog" show-auto-search-option />
</template>

<script setup lang="ts">
import { onClickOutside, useDebounceFn, useFullscreen, useWindowSize } from '@vueuse/core'
import { onClickOutside, useDebounceFn, useFullscreen, useStorage, useWindowSize } from '@vueuse/core'
import { computed, markRaw, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import GlassModal from '@/components/GlassModal.vue'
import Tutorial from '@/components/Tutorial.vue'
import VehicleDiscoveryDialog from '@/components/VehicleDiscoveryDialog.vue'
import VideoLibraryModal from '@/components/VideoLibraryModal.vue'
import { useInteractionDialog } from '@/composables/interactionDialog'
import {
availableCockpitActions,
registerActionCallback,
unregisterActionCallback,
} from '@/libs/joystick/protocols/cockpit-actions'
import { isElectron } from '@/libs/utils'
import About from './components/About.vue'
import AltitudeSlider from './components/AltitudeSlider.vue'
Expand Down Expand Up @@ -720,6 +723,19 @@ onBeforeUnmount(() => {
// Dynamic styles
const currentTopBarHeightPixels = computed(() => `${widgetStore.currentTopBarHeightPixels}px`)
const currentBottomBarHeightPixels = computed(() => `${widgetStore.currentBottomBarHeightPixels}px`)
const showDiscoveryDialog = ref(false)
const preventAutoSearch = useStorage('cockpit-prevent-auto-vehicle-discovery-dialog', false)
onMounted(() => {
if (!isElectron() || preventAutoSearch.value) return
// Wait 5 seconds to check if we're connected to a vehicle
setTimeout(() => {
if (vehicleStore.isVehicleOnline) return
showDiscoveryDialog.value = true
}, 5000)
})
</script>

<style>
Expand Down
2 changes: 1 addition & 1 deletion src/components/InteractionDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const slots = useSlots()
/**
* Interface to an array of buttons for the Interaction Dialog's footer
*/
interface Action {
export interface Action {
/**
* Button Text
*/
Expand Down
151 changes: 151 additions & 0 deletions src/components/VehicleDiscoveryDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<template>
<InteractionDialog
v-model="isOpen"
:title="searching ? 'Searching for vehicles...' : 'Vehicle Discovery'"
:actions="dialogActions"
:persistent="searching"
:variant="'text-only'"
>
<template #content>
<div v-if="props.showAutoSearchOption && preventAutoSearch">
<div class="text-sm mb-4">You can still search for vehicles in the general configuration menu.</div>
</div>
<div v-else class="flex flex-col items-center justify-center gap-4 min-w-[300px] min-h-[100px]">
<div v-if="searching" class="flex items-center gap-2">
<v-progress-circular indeterminate />
<span>Searching for vehicles in your network...</span>
</div>

<div v-else-if="vehicles.length > 0" class="flex flex-col gap-2 mb-6">
<div class="text-sm mb-2">Found the following vehicles:</div>
<div v-for="vehicle in vehicles" :key="vehicle.address" class="flex items-center gap-2">
<v-btn variant="outlined" class="w-full justify-start" @click="selectVehicle(vehicle.address)">
{{ vehicle.name }}
<span class="text-xs ml-2 opacity-50">({{ vehicle.address }})</span>
</v-btn>
</div>
</div>

<div v-else-if="searched" class="text-sm">No vehicles found in your network.</div>

<div v-if="!searching && !searched" class="flex flex-col gap-2 items-center justify-center text-center">
<p v-if="props.showAutoSearchOption" class="font-bold">It looks like you're not connected to a vehicle!</p>
<p>The vehicle discovery feature can help you find your vehicle and connect to it.</p>
<p>It will search for vehicles in your network and let you select one.</p>
</div>

<div v-if="!searching" class="flex justify-center items-center">
<v-btn variant="outlined" :disabled="searching" class="mb-5" @click="searchVehicles">
{{ searched ? 'Search again' : 'Search for vehicles' }}
</v-btn>
</div>
</div>
</template>
</InteractionDialog>
</template>

<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { ref, watch } from 'vue'
import { useSnackbar } from '@/composables/snackbar'
import vehicleDiscover, { NetworkVehicle } from '@/libs/electron/vehicle-discovery'
import { useMainVehicleStore } from '@/stores/mainVehicle'
import InteractionDialog, { Action } from './InteractionDialog.vue'
const props = defineProps<{
/**
*
*/
modelValue: boolean
/**
*
*/
showAutoSearchOption?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const { showSnackbar } = useSnackbar()
const mainVehicleStore = useMainVehicleStore()
const discoveryService = vehicleDiscover
const isOpen = ref(props.modelValue)
const searching = ref(false)
const searched = ref(false)
const vehicles = ref<NetworkVehicle[]>([])
const preventAutoSearch = useStorage('cockpit-prevent-auto-vehicle-discovery-dialog', false)
const originalActions = [
{
text: 'Close',
action: () => {
isOpen.value = false
},
},
]
if (props.showAutoSearchOption) {
originalActions.unshift({
text: "Don't show again",
action: () => preventFutureAutoSearchs(),
})
}
const dialogActions = ref<Action[]>(originalActions)
watch(
() => props.modelValue,
(value) => {
isOpen.value = value
}
)
watch(isOpen, (value) => {
emit('update:modelValue', value)
})
const searchVehicles = async (): Promise<void> => {
searching.value = true
disableButtons()
vehicles.value = await discoveryService.findVehicles()
searching.value = false
enableButtons()
searched.value = true
}
const selectVehicle = (address: string): void => {
mainVehicleStore.globalAddress = address
isOpen.value = false
showSnackbar({ message: 'Vehicle address updated', variant: 'success', duration: 5000 })
}
const preventFutureAutoSearchs = (): void => {
preventAutoSearch.value = true
disableButtons()
setTimeout(() => {
isOpen.value = false
}, 5000)
}
const disableButtons = (): void => {
dialogActions.value = originalActions.map((action) => ({ ...action, disabled: true }))
}
const enableButtons = (): void => {
dialogActions.value = originalActions
}
watch(isOpen, (isNowOpen) => {
if (isNowOpen) return
setTimeout(() => {
vehicles.value = []
searching.value = false
searched.value = false
}, 1000)
})
</script>
10 changes: 10 additions & 0 deletions src/libs/cosmos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ declare global {
unregisterActionCallback: typeof unregisterActionCallback
executeActionCallback: typeof executeActionCallback
}
/**
* Electron API exposed through preload script
*/
electronAPI?: {
/**
* Get network information from the main process
* @returns Promise containing subnet information
*/
getNetworkInfo: () => Promise<{ subnet: string }>
}
}
/* eslint-enable jsdoc/require-jsdoc */
}
Expand Down
Loading

0 comments on commit 519bbb8

Please sign in to comment.