Skip to content

Commit

Permalink
vehicle-discovery: Fix implementation to allow finding IPs generated …
Browse files Browse the repository at this point in the history
…by a DHCP server in the vehicle

The vehicle usually has some IPs generated by it's own servers (e.g.: 192.168.3.1 for USB C connections), which offers a dynamic IP for the topside computer. With this change, those IPs are also findable.

This commit should be in the original PR, but got lost on the rebase process.
  • Loading branch information
rafaellehmkuhl authored and ArturoManzoli committed Dec 9, 2024
1 parent a982448 commit c6c4ace
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 73 deletions.
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[]
}

0 comments on commit c6c4ace

Please sign in to comment.