Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vehicle-discovery: Allow finding IPs generated by a DHCP server in the vehicle #1495

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion electron/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { contextBridge, ipcRenderer } from 'electron'

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

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

import { NetworkInfo } from '../../src/types/network'
/**
* Get the network information
* @returns {NetworkInfo} The network information
*/
const getNetworkInfo = (): NetworkInfo => {
const nets = networkInterfaces()
const getInfoOnSubnets = (): NetworkInfo[] => {
const allSubnets = 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('.') }
}
}
const ipv4Subnets = Object.entries(allSubnets)
.flatMap(([_, nets]) => {
return nets.map((net) => ({ ...net, interfaceName: _ }))
})
.filter((net) => net.family === 'IPv4')
.filter((net) => !net.internal)

if (ipv4Subnets.length === 0) {
throw new Error('No network interfaces found.')
}

throw new Error('No network interface found.')
return ipv4Subnets.map((subnet) => {
// TODO: Use the mask to calculate the available addresses. The current implementation is not correct for anything else than /24.
const subnetPrefix = subnet.address.split('.').slice(0, 3).join('.')
const availableAddresses: string[] = []
for (let i = 1; i <= 254; i++) {
availableAddresses.push(`${subnetPrefix}.${i}`)
}

return {
topSideAddress: subnet.address,
macAddress: subnet.mac,
interfaceName: subnet.interfaceName,
availableAddresses,
}
})
}

/**
* Setup the network service
*/
export const setupNetworkService = (): void => {
ipcMain.handle('get-network-info', getNetworkInfo)
ipcMain.handle('get-info-on-subnets', getInfoOnSubnets)
}
8 changes: 6 additions & 2 deletions src/components/VehicleDiscoveryDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@

<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 class="max-w-[25rem] mb-2">This tool allows you to locate and connect to vehicles within your network.</p>
<p class="max-w-[25rem] mb-2">
This tool allows you to locate and connect to BlueOS vehicles within your network.
</p>
</div>

<div v-if="!searching" class="flex justify-center items-center">
Expand All @@ -49,6 +51,7 @@ import { ref, watch } from 'vue'

import { useSnackbar } from '@/composables/snackbar'
import vehicleDiscover, { NetworkVehicle } from '@/libs/electron/vehicle-discovery'
import { reloadCockpit } from '@/libs/utils'
import { useMainVehicleStore } from '@/stores/mainVehicle'

import InteractionDialog, { Action } from './InteractionDialog.vue'
Expand Down Expand Up @@ -116,9 +119,10 @@ const searchVehicles = async (): Promise<void> => {
searched.value = true
}

const selectVehicle = (address: string): void => {
const selectVehicle = async (address: string): Promise<void> => {
mainVehicleStore.globalAddress = address
isOpen.value = false
await reloadCockpit()
showSnackbar({ message: 'Vehicle address updated', variant: 'success', duration: 5000 })
}

Expand Down
4 changes: 3 additions & 1 deletion src/libs/cosmos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { isBrowser } from 'browser-or-node'

import { NetworkInfo } from '@/types/network'

import {
cockpitActionVariableData,
createCockpitActionVariable,
Expand Down Expand Up @@ -110,7 +112,7 @@ declare global {
* Get network information from the main process
* @returns Promise containing subnet information
*/
getNetworkInfo: () => Promise<{ subnet: string }>
getInfoOnSubnets: () => Promise<NetworkInfo[]>
}
}
/* eslint-enable jsdoc/require-jsdoc */
Expand Down
84 changes: 37 additions & 47 deletions src/libs/electron/vehicle-discovery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { getStatus } from '../blueos'
import ky from 'ky'

import { NetworkInfo } from '@/types/network'

import { isElectron } from '../utils'

/**
Expand Down Expand Up @@ -41,47 +44,20 @@ class VehicleDiscover {
private async checkAddress(address: string): Promise<NetworkVehicle | null> {
try {
// First check if the vehicle is online
const hasRespondingStatusEndpoint = await getStatus(address)
if (!hasRespondingStatusEndpoint) {
return null
}
const statusResponse = await ky.get(`http://${address}/status`, { timeout: 3000 })
if (!statusResponse.ok) return null

// Try to get the vehicle name
try {
const response = await fetch(`http://${address}/beacon/v1.0/vehicle_name`)
if (!response.ok) {
return null
}
const name = await response.text()
return { address, name }
} catch {
// If we can't get the name, it's because it's not a vehicle (or maybe BlueOS's Beacon service is not running)
return null
}
const nameResponse = await ky.get(`http://${address}/beacon/v1.0/vehicle_name`, { timeout: 5000 })
if (!nameResponse.ok) return null
const name = await nameResponse.text()
return { address, name }
} catch {
// If we can't get the status, it's because the vehicle is not online
// If we can't get the name, it's because it's not a vehicle (or maybe BlueOS's Beacon service is not running)
return null
}
}

/**
* Get the local subnet
* @returns {string | null} The local subnet, or null if not running in Electron
*/
private async getLocalSubnet(): Promise<string> {
if (!isElectron() || !window.electronAPI?.getNetworkInfo) {
const msg = 'For technical reasons, getting information about the local subnet is only available in Electron.'
throw new Error(msg)
}

try {
const { subnet } = await window.electronAPI.getNetworkInfo()
return subnet
} catch (error) {
throw new Error(`Failed to get information about the local subnet. ${error}`)
}
}

/**
* Find vehicles on the local network
* @returns {NetworkVehicle[]} The vehicles found
Expand All @@ -96,23 +72,37 @@ class VehicleDiscover {
}

const search = async (): Promise<NetworkVehicle[]> => {
const subnet = await this.getLocalSubnet()

if (!subnet) {
throw new Error('Failed to get information about the local subnet.')
if (!isElectron() || !window.electronAPI?.getInfoOnSubnets) {
const msg = 'For technical reasons, getting information about the local subnet is only available in Electron.'
throw new Error(msg)
}

const promises: Promise<NetworkVehicle | null>[] = []
let localSubnets: NetworkInfo[] | undefined
try {
localSubnets = await window.electronAPI.getInfoOnSubnets()
} catch (error) {
throw new Error(`Failed to get information about the local subnets. ${error}`)
}

// Check all IPs in the subnet
for (let i = 1; i <= 254; i++) {
const address = `${subnet}.${i}`
promises.push(this.checkAddress(address))
if (localSubnets.length === 0) {
throw new Error('Failed to get information about the local subnets.')
}

const vehiclesFound = await Promise.all(promises).then((results) => {
return results.filter((result): result is NetworkVehicle => result !== null)
})
const vehiclesFound: NetworkVehicle[] = []
for (const subnet of localSubnets) {
const topSideAddress = subnet.topSideAddress
const possibleAddresses = subnet.availableAddresses.filter((address) => address !== topSideAddress)

const promises: Promise<NetworkVehicle | null>[] = possibleAddresses.map((address) => {
return this.checkAddress(address)
})

const vehicles = await Promise.all(promises).then((results) => {
return results.filter((result): result is NetworkVehicle => result !== null)
})

vehiclesFound.push(...vehicles)
}

this.currentSearch = undefined

Expand Down
21 changes: 21 additions & 0 deletions src/types/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Information about the network
*/
export interface NetworkInfo {
/**
* The top side address of the local machine
*/
topSideAddress: string
/**
* The MAC address of the local machine
*/
macAddress: string
/**
* The name of the network interface
*/
interfaceName: string
/**
* The CIDR of the local machine
*/
availableAddresses: string[]
}
Loading