Skip to content

Commit a3d254f

Browse files
frontend: detect and list other vehicles on blueos
1 parent 04d31d3 commit a3d254f

File tree

3 files changed

+385
-11
lines changed

3 files changed

+385
-11
lines changed

core/frontend/src/components/app/VehicleBanner.vue

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,19 @@
1818
>
1919
({{ system_id }})
2020
</span>
21+
</p>
22+
<div class="action-buttons-container">
2123
<v-btn
22-
class="mx-2 edit-icon"
24+
class="mx-1"
2325
fab
2426
dark
2527
x-small
2628
@click="openDialog"
2729
>
28-
<v-icon>
29-
mdi-pencil
30-
</v-icon>
30+
<v-icon>mdi-pencil</v-icon>
3131
</v-btn>
32-
</p>
32+
<VehiclePicker />
33+
</div>
3334
<v-spacer />
3435
<image-picker
3536
size="35px"
@@ -74,11 +75,13 @@ import bag from '@/store/bag'
7475
import beacon from '@/store/beacon'
7576
7677
import ImagePicker from './ImagePicker.vue'
78+
import VehiclePicker from './VehiclePicker.vue'
7779
7880
export default Vue.extend({
7981
name: 'VehicleBanner',
8082
components: {
8183
ImagePicker,
84+
VehiclePicker,
8285
},
8386
data() {
8487
return {
@@ -166,7 +169,7 @@ export default Vue.extend({
166169
})
167170
</script>
168171
<style scoped>
169-
#vehicle-name:hover .edit-icon{
172+
#vehicle-banner:hover .action-buttons-container{
170173
display: inline-flex;
171174
}
172175
@@ -181,10 +184,13 @@ export default Vue.extend({
181184
margin-bottom: 0 !important;
182185
position: relative;
183186
}
184-
.edit-icon {
187+
188+
.action-buttons-container {
185189
display: none;
186-
right: 0;
187-
bottom: 0;
188-
position: absolute;
190+
position: fixed;
191+
right: 70px;
192+
z-index: 10;
193+
align-items: center;
194+
gap: 4px;
189195
}
190196
</style>
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
<template>
2+
<div class="vehicle-picker">
3+
<v-btn
4+
:loading="loading"
5+
class="mx-1"
6+
fab
7+
dark
8+
x-small
9+
@click="toggleDropdown"
10+
>
11+
<v-icon>{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
12+
</v-btn>
13+
14+
<v-menu
15+
v-model="expanded"
16+
:close-on-content-click="false"
17+
offset-y
18+
min-width="150"
19+
>
20+
<template #activator="{ }" />
21+
22+
<v-card>
23+
<v-card-title class="subtitle-2 pa-2">
24+
Other Vehicles detected on your network
25+
<v-spacer />
26+
<v-btn
27+
icon
28+
x-small
29+
@click="refreshServices"
30+
>
31+
<v-icon small>
32+
mdi-refresh
33+
</v-icon>
34+
</v-btn>
35+
</v-card-title>
36+
37+
<v-divider />
38+
39+
<v-list v-if="availableVehicles.length > 0" dense>
40+
<v-list-item
41+
v-for="vehicle in availableVehicles"
42+
:key="vehicle.hostname"
43+
>
44+
<v-list-item-avatar>
45+
<v-img
46+
v-if="vehicle.imagePath"
47+
:src="vehicle.imagePath"
48+
:alt="vehicle.hostname"
49+
aspect-ratio="1"
50+
class="vehicle-image"
51+
>
52+
<template #error>
53+
<v-icon color="primary">
54+
mdi-submarine
55+
</v-icon>
56+
</template>
57+
</v-img>
58+
<v-icon v-else color="primary">
59+
mdi-submarine
60+
</v-icon>
61+
</v-list-item-avatar>
62+
<v-list-item-content>
63+
<v-list-item-title>
64+
<span class="vehicle-name">
65+
{{ vehicle.hostname }}
66+
</span>
67+
{{ allMyIps.includes(vehicle.ips[0]) ? '(This Vehicle)' : '' }}
68+
</v-list-item-title>
69+
<v-list-item-title v-for="ip in vehicle.ips" :key="ip" class="caption" @click="navigateToVehicle(ip)">
70+
{{ ip }}
71+
<v-icon x-small color="primary">
72+
mdi-launch
73+
</v-icon>
74+
</v-list-item-title>
75+
</v-list-item-content>
76+
</v-list-item>
77+
</v-list>
78+
79+
<v-card-text v-else-if="!loading" class="text-center text--secondary">
80+
<div v-if="error">
81+
<v-icon color="error">
82+
mdi-alert-circle
83+
</v-icon>
84+
<div class="caption">
85+
{{ error }}
86+
</div>
87+
</div>
88+
<div v-else>
89+
No vehicles discovered
90+
</div>
91+
</v-card-text>
92+
93+
<v-card-text v-else class="text-center">
94+
<v-progress-circular
95+
indeterminate
96+
size="24"
97+
width="2"
98+
/>
99+
<div class="caption mt-2">
100+
Discovering vehicles...
101+
</div>
102+
</v-card-text>
103+
</v-card>
104+
</v-menu>
105+
</div>
106+
</template>
107+
108+
<script lang="ts">
109+
import axios from 'axios'
110+
import Vue from 'vue'
111+
112+
import system_information from '@/store/system-information'
113+
114+
interface DiscoveredServices {
115+
[ip: string]: string[]
116+
}
117+
118+
interface Vehicle {
119+
ips: string[]
120+
hostname: string
121+
imagePath?: string
122+
}
123+
124+
export default Vue.extend({
125+
name: 'VehiclePicker',
126+
data() {
127+
return {
128+
expanded: false,
129+
loading: false,
130+
error: null as string | null,
131+
availableVehicles: [] as Vehicle[],
132+
}
133+
},
134+
computed: {
135+
allMyIps(): string[] {
136+
return system_information.system?.network.flatMap((network) => network.ips.map((ip) => ip.split('/')[0])) || []
137+
},
138+
},
139+
methods: {
140+
toggleDropdown() {
141+
this.expanded = !this.expanded
142+
if (this.expanded && this.availableVehicles.length === 0) {
143+
this.fetchDiscoveredServices()
144+
}
145+
},
146+
147+
async fetchDiscoveredServices() {
148+
this.loading = true
149+
this.error = null
150+
this.availableVehicles = []
151+
152+
try {
153+
const response = await axios.get('/beacon/v1.0/discovered_services', {
154+
timeout: 10000,
155+
})
156+
157+
const discoveredServices: DiscoveredServices = response.data
158+
const ips = Object.keys(discoveredServices)
159+
160+
if (ips.length === 0) {
161+
this.error = 'No vehicles discovered'
162+
this.loading = false
163+
return
164+
}
165+
166+
let completedRequests = 0
167+
168+
ips.forEach(async (ip) => {
169+
try {
170+
const hostnameResponse = await axios.get(`http://${ip}/beacon/v1.0/hostname`, {
171+
timeout: 5000,
172+
})
173+
174+
let imagePath: string | undefined
175+
try {
176+
const imageResponse = await axios.get(`http://${ip}/bag/v1.0/get/vehicle.image_path`, {
177+
timeout: 3000,
178+
})
179+
imagePath = `http://${ip}${imageResponse.data.url}`
180+
} catch {
181+
// Image not available, use fallback
182+
}
183+
184+
const newVehicle: Vehicle = {
185+
ips: [ip],
186+
hostname: hostnameResponse.data,
187+
imagePath,
188+
}
189+
190+
const existingVehicleIndex = this.availableVehicles.findIndex(
191+
(v) => v.hostname === newVehicle.hostname,
192+
)
193+
194+
if (existingVehicleIndex >= 0) {
195+
this.availableVehicles[existingVehicleIndex].ips.push(...newVehicle.ips)
196+
if (newVehicle.imagePath && !this.availableVehicles[existingVehicleIndex].imagePath) {
197+
this.availableVehicles[existingVehicleIndex].imagePath = newVehicle.imagePath
198+
}
199+
} else {
200+
this.availableVehicles.push(newVehicle)
201+
}
202+
203+
this.availableVehicles.sort((a, b) => a.hostname.localeCompare(b.hostname))
204+
} catch (error) {
205+
console.warn(`Failed to fetch hostname for ${ip}:`, error)
206+
} finally {
207+
completedRequests += 1
208+
if (completedRequests === ips.length) {
209+
this.loading = false
210+
if (this.availableVehicles.length === 0) {
211+
this.error = 'No accessible vehicles found'
212+
}
213+
}
214+
}
215+
})
216+
} catch (error) {
217+
console.error('Failed to fetch discovered services:', error)
218+
this.error = 'Failed to discover vehicles'
219+
this.availableVehicles = []
220+
this.loading = false
221+
}
222+
},
223+
224+
async refreshServices() {
225+
await this.fetchDiscoveredServices()
226+
},
227+
228+
navigateToVehicle(ip: string) {
229+
window.open(`http://${ip}`, '_blank')
230+
this.expanded = false
231+
},
232+
},
233+
})
234+
</script>
235+
236+
<style scoped>
237+
.vehicle-picker {
238+
display: inline-block;
239+
}
240+
241+
.caption:hover {
242+
background-color: rgba(0, 0, 0, 0.04);
243+
cursor: pointer;
244+
}
245+
246+
.vehicle-image {
247+
border-radius: 4px;
248+
}
249+
250+
.v-list-item:hover {
251+
background-color: rgba(0, 0, 0, 0.04);
252+
}
253+
254+
.vehicle-name {
255+
font-size: 1.2rem;
256+
}
257+
</style>

0 commit comments

Comments
 (0)