Skip to content

Commit

Permalink
Widgets: add support for Widget discovery from BlueOS
Browse files Browse the repository at this point in the history
  • Loading branch information
Williangalvani committed Dec 17, 2024
1 parent ab5a1f9 commit 142a515
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 21 deletions.
53 changes: 41 additions & 12 deletions src/components/EditMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -485,27 +485,22 @@
class="flex items-center justify-between w-full h-full gap-3 overflow-x-auto text-white -mb-1 pr-2 cursor-pointer"
>
<div
v-for="widgetType in availableWidgetTypes"
:key="widgetType"
v-for="widget in allAvailableWidgets"
:key="widget.name"
class="flex flex-col items-center justify-between rounded-md bg-[#273842] hover:brightness-125 h-[90%] aspect-square cursor-pointer elevation-4"
draggable="true"
@dragstart="onRegularWidgetDragStart"
@dragend="onRegularWidgetDragEnd(widgetType)"
@dragend="onRegularWidgetDragEnd(widget)"
>
<v-tooltip text="Drag to add" location="top" theme="light">
<template #activator="{ props: tooltipProps }">
<div />
<img
v-bind="tooltipProps"
:src="widgetImages[widgetType]"
alt="widget-icon"
class="p-4 max-h-[75%] max-w-[95%]"
/>
<img v-bind="tooltipProps" :src="widget.icon" alt="widget-icon" class="p-4 max-h-[75%] max-w-[95%]" />
<div
class="flex items-center justify-center w-full p-1 transition-all bg-[#3B78A8] rounded-b-md text-white"
>
<span class="whitespace-normal text-center">{{
widgetType.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (str) => str.toUpperCase())
widget.name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (str) => str.toUpperCase())
}}</span>
</div>
</template>
Expand Down Expand Up @@ -648,6 +643,7 @@ import URLVideoPlayerImg from '@/assets/widgets/URLVideoPlayer.png'
import VideoPlayerImg from '@/assets/widgets/VideoPlayer.png'
import VirtualHorizonImg from '@/assets/widgets/VirtualHorizon.png'
import { useInteractionDialog } from '@/composables/interactionDialog'
import { getWidgetsFromBlueOS } from '@/libs/blueos'
import { MavType } from '@/libs/connection/m2r/messages/mavlink2rest-enum'
import { isHorizontalScroll } from '@/libs/utils'
import { useAppInterfaceStore } from '@/stores/appInterface'
Expand All @@ -657,6 +653,8 @@ import {
type View,
type Widget,
CustomWidgetElementType,
ExternalWidgetSetupInfo,
InternalWidgetSetupInfo,
MiniWidgetType,
WidgetType,
} from '@/types/widgets'
Expand Down Expand Up @@ -685,6 +683,8 @@ const toggleDial = (): void => {
const forceUpdate = ref(0)
const ExternalWidgetSetupInfos = ref<ExternalWidgetSetupInfo[]>([])
watch(
() => store.currentView.widgets,
() => {
Expand Down Expand Up @@ -712,7 +712,31 @@ watch(
}
)
const availableWidgetTypes = computed(() => Object.values(WidgetType))
const availableWidgetTypes = computed(() =>
Object.values(WidgetType).map((widgetType) => {
return {
component: widgetType,
name: widgetType,
icon: widgetImages[widgetType] as string,
options: {},
}
})
)
const allAvailableWidgets = computed(() => {
return [
...ExternalWidgetSetupInfos.value.map((widget) => ({
component: WidgetType.IFrame,
icon: widget.iframe_icon,
name: widget.name,
options: {
source: widget.iframe_url,
},
})),
...availableInternalWidgets.value,
]
})
const availableMiniWidgetTypes = computed(() =>
Object.values(MiniWidgetType).map((widgetType) => ({
component: widgetType,
Expand Down Expand Up @@ -925,6 +949,10 @@ const miniWidgetsContainerOptions = ref<UseDraggableOptions>({
})
useDraggable(availableMiniWidgetsContainer, availableMiniWidgetTypes, miniWidgetsContainerOptions)
const getExternalWidgetSetupInfos = async (): Promise<void> => {
ExternalWidgetSetupInfos.value = await getWidgetsFromBlueOS()
}
// @ts-ignore: Documentation is not clear on what generic should be passed to 'UseDraggableOptions'
const customWidgetElementContainerOptions = ref<UseDraggableOptions>({
animation: '150',
Expand All @@ -938,6 +966,7 @@ useDraggable(
)
onMounted(() => {
getExternalWidgetSetupInfos()
const widgetContainers = [
availableWidgetsContainer.value,
availableMiniWidgetsContainer.value,
Expand Down Expand Up @@ -989,7 +1018,7 @@ const onRegularWidgetDragStart = (event: DragEvent): void => {
}
}
const onRegularWidgetDragEnd = (widgetType: WidgetType): void => {
const onRegularWidgetDragEnd = (widgetType: ExtendedWidget): void => {
store.addWidget(widgetType, store.currentView)
const widgetCards = document.querySelectorAll('[draggable="true"]')
Expand Down
61 changes: 61 additions & 0 deletions src/libs/blueos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import ky, { HTTPError } from 'ky'

import { useMainVehicleStore } from '@/stores/mainVehicle'
import { ExternalWidgetSetupInfo } from '@/types/widgets'

/**
* Cockpits extra json format. Taken from extensions in BlueOS and (eventually) other places
*/
interface ExtrasJson {
/**
* The version of the cockpit API that the extra json is compatible with
*/
target_cockpit_api_version: string
/**
* The target system that the extra json is compatible with, in our case, "cockpit"
*/
target_system: string
/**
* A list of widgets that the extra json contains. src/types/widgets.ts
*/
widgets: ExternalWidgetSetupInfo[]
}

export const NoPathInBlueOsErrorName = 'NoPathInBlueOS'

const defaultTimeout = 10000
Expand Down Expand Up @@ -30,6 +51,46 @@ export const getKeyDataFromCockpitVehicleStorage = async (
return await getBagOfHoldingFromVehicle(vehicleAddress, `cockpit/${storageKey}`)
}

export const getWidgetsFromBlueOS = async (): Promise<ExternalWidgetSetupInfo[]> => {
const vehicleStore = useMainVehicleStore()

// Wait until we have a global address
while (vehicleStore.globalAddress === undefined) {
await new Promise((r) => setTimeout(r, 1000))
}
const options = { timeout: defaultTimeout, retry: 0 }
const services = (await ky
.get(`http://${vehicleStore.globalAddress}/helper/v1.0/web_services`, options)
.json()) as Record<string, any>
// first we gather all the extra jsons with the cockpit key
const extraWidgets = await services.reduce(
async (accPromise: Promise<ExternalWidgetSetupInfo[]>, service: Record<string, any>) => {
const acc = await accPromise
const worksInRelativePaths = service.metadata?.works_in_relative_paths
if (service.metadata?.extras?.cockpit === undefined) {
return acc
}
const baseUrl = worksInRelativePaths
? `http://${vehicleStore.globalAddress}/extensionv2/${service.metadata.sanitized_name}`
: `http://${vehicleStore.globalAddress}:${service.port}`
const fullUrl = baseUrl + service.metadata?.extras?.cockpit

const extraJson: ExtrasJson = await ky.get(fullUrl, options).json()
const widgets: ExternalWidgetSetupInfo[] = extraJson.widgets.map((widget) => {
return {
...widget,
iframe_url: baseUrl + widget.iframe_url,
iframe_icon: baseUrl + widget.iframe_icon,
}
})
return acc.concat(widgets)
},
Promise.resolve([] as ExternalWidgetSetupInfo[])
)

return extraWidgets
}

export const setBagOfHoldingOnVehicle = async (
vehicleAddress: string,
bagName: string,
Expand Down
19 changes: 10 additions & 9 deletions src/stores/widgetManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
type Widget,
CustomWidgetElement,
CustomWidgetElementContainer,
InternalWidgetSetupInfo,
MiniWidgetManagerVars,
validateProfile,
validateView,
Expand Down Expand Up @@ -555,22 +556,22 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {

/**
* Add widget with given type to given view
* @param { WidgetType } widgetType - Type of the widget
* @param { WidgetType } widget - Type of the widget
* @param { View } view - View
*/
function addWidget(widgetType: WidgetType, view: View): void {
function addWidget(widget: InternalWidgetSetupInfo, view: View): void {
const widgetHash = uuid4()

const widget = {
const newWidget = {
hash: widgetHash,
name: widgetType,
component: widgetType,
name: widget.name,
component: widget.component,
position: { x: 0.4, y: 0.32 },
size: { width: 0.2, height: 0.36 },
options: {},
options: widget.options,
}

if (widgetType === WidgetType.CustomWidgetBase) {
if (widget.component === WidgetType.CustomWidgetBase) {
widget.options = {
elementContainers: defaultCustomWidgetContainers,
columns: 1,
Expand All @@ -581,8 +582,8 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
}
}

view.widgets.unshift(widget)
Object.assign(widgetManagerVars(widget.hash), {
view.widgets.unshift(newWidget)
Object.assign(widgetManagerVars(newWidget.hash), {
...defaultWidgetManagerVars,
...{ allowMoving: true },
})
Expand Down
41 changes: 41 additions & 0 deletions src/types/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@ import { CockpitAction } from '@/libs/joystick/protocols/cockpit-actions'

import type { Point2D, SizeRect2D } from './general'

/**
* Widget configuration object as received from BlueOS or another external source
*/
export interface ExternalWidgetSetupInfo {
/**
* Name of the widget, this is displayed on edit mode widget browser
*/
name: string
/**
* The URL at which the widget is located
* This is expected to be an absolute url
*/
iframe_url: string

/**
* The icon of the widget, this is displayed on the widget browser
*/
iframe_icon: string
}

/**
* Internal data used for setting up a new widget. This includes WidgetType, a custom name, options, and icon
*/ export interface InternalWidgetSetupInfo {
/**
* Widget type
*/
component: WidgetType
/**
* Widget name, this will be displayed on edit mode
*/
name: string
/**
* Widget options, this is the configuration that will be passed to the widget when it is created
*/
options: Record<string, unknown>
/**
* Widget icon, this is the icon that will be displayed on the widget browser
*/
icon: string
}

/**
* Available components to be used in the Widget system
* The enum value is equal to the component's filename, without the '.vue' extension
Expand Down

0 comments on commit 142a515

Please sign in to comment.