From c6c4ace4ba72d4529a054a4d7f3c237d1f0743d4 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Fri, 22 Nov 2024 13:26:42 -0300 Subject: [PATCH] vehicle-discovery: Fix implementation to allow finding IPs generated 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. --- electron/preload.ts | 2 +- electron/services/network.ts | 50 ++++++++------ src/components/VehicleDiscoveryDialog.vue | 8 ++- src/libs/cosmos.ts | 4 +- src/libs/electron/vehicle-discovery.ts | 84 ++++++++++------------- src/types/network.ts | 21 ++++++ 6 files changed, 96 insertions(+), 73 deletions(-) create mode 100644 src/types/network.ts diff --git a/electron/preload.ts b/electron/preload.ts index bfdd3fe8c..4312004c1 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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'), }) diff --git a/electron/services/network.ts b/electron/services/network.ts index 991b0f60f..4f56dbbf4 100644 --- a/electron/services/network.ts +++ b/electron/services/network.ts @@ -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) } diff --git a/src/components/VehicleDiscoveryDialog.vue b/src/components/VehicleDiscoveryDialog.vue index cd09bc93e..456f5fb99 100644 --- a/src/components/VehicleDiscoveryDialog.vue +++ b/src/components/VehicleDiscoveryDialog.vue @@ -30,7 +30,9 @@

It looks like you're not connected to a vehicle!

-

This tool allows you to locate and connect to vehicles within your network.

+

+ This tool allows you to locate and connect to BlueOS vehicles within your network. +

@@ -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' @@ -116,9 +119,10 @@ const searchVehicles = async (): Promise => { searched.value = true } -const selectVehicle = (address: string): void => { +const selectVehicle = async (address: string): Promise => { mainVehicleStore.globalAddress = address isOpen.value = false + await reloadCockpit() showSnackbar({ message: 'Vehicle address updated', variant: 'success', duration: 5000 }) } diff --git a/src/libs/cosmos.ts b/src/libs/cosmos.ts index 5463d6d2b..41cc98050 100644 --- a/src/libs/cosmos.ts +++ b/src/libs/cosmos.ts @@ -1,5 +1,7 @@ import { isBrowser } from 'browser-or-node' +import { NetworkInfo } from '@/types/network' + import { cockpitActionVariableData, createCockpitActionVariable, @@ -110,7 +112,7 @@ declare global { * Get network information from the main process * @returns Promise containing subnet information */ - getNetworkInfo: () => Promise<{ subnet: string }> + getInfoOnSubnets: () => Promise } } /* eslint-enable jsdoc/require-jsdoc */ diff --git a/src/libs/electron/vehicle-discovery.ts b/src/libs/electron/vehicle-discovery.ts index 0399eea26..bc7f0f3f4 100644 --- a/src/libs/electron/vehicle-discovery.ts +++ b/src/libs/electron/vehicle-discovery.ts @@ -1,4 +1,7 @@ -import { getStatus } from '../blueos' +import ky from 'ky' + +import { NetworkInfo } from '@/types/network' + import { isElectron } from '../utils' /** @@ -41,47 +44,20 @@ class VehicleDiscover { private async checkAddress(address: string): Promise { 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 { - 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 @@ -96,23 +72,37 @@ class VehicleDiscover { } const search = async (): Promise => { - 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[] = [] + 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[] = 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 diff --git a/src/types/network.ts b/src/types/network.ts new file mode 100644 index 000000000..590716ccb --- /dev/null +++ b/src/types/network.ts @@ -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[] +}