diff --git a/src/App.vue b/src/App.vue index 9f4d4e22a..fc19580ca 100644 --- a/src/App.vue +++ b/src/App.vue @@ -343,6 +343,7 @@ import { useAppInterfaceStore } from './stores/appInterface' import { useMainVehicleStore } from './stores/mainVehicle' import { useWidgetManagerStore } from './stores/widgetManager' import { ConfigComponent } from './types/general' +import ConfigurationActionsView from './views/ConfigurationActionsView.vue' import ConfigurationAlertsView from './views/ConfigurationAlertsView.vue' import ConfigurationDevelopmentView from './views/ConfigurationDevelopmentView.vue' import ConfigurationGeneralView from './views/ConfigurationGeneralView.vue' @@ -410,6 +411,11 @@ const configMenu = [ title: 'Mission', component: markRaw(ConfigurationMissionView) as ConfigComponent, }, + { + icon: 'mdi-run-fast', + title: 'Actions', + component: markRaw(ConfigurationActionsView) as ConfigComponent, + }, ] watch( diff --git a/src/libs/actions/data-lake.ts b/src/libs/actions/data-lake.ts new file mode 100644 index 000000000..00f4d9815 --- /dev/null +++ b/src/libs/actions/data-lake.ts @@ -0,0 +1,107 @@ +/** + * A parameter for a Cockpit action + * @param { string } name - The name of the parameter + * @param { 'string' | 'number' | 'boolean' } type - The type of the parameter (string, number or boolean) + * @param { string } description - What the parameter does or means + * @param { string | number | boolean } defaultValue - The default value of the parameter + * @param { boolean } required - Whether the parameter is required or not + * @param { (string | number)[]? } options - The options for the parameter (only if type is string or number). + * @param { number? } min - The minimum value for the parameter (only if type is number). + * @param { number? } max - The maximum value for the parameter (only if type is number). + */ +class CockpitActionParameter { + id: string + name: string + type: 'string' | 'number' | 'boolean' + required: boolean + description?: string + defaultValue?: string | number | boolean + options?: (string | number)[] + min?: number + max?: number + // eslint-disable-next-line jsdoc/require-jsdoc + constructor( + id: string, + name: string, + type: 'string' | 'number' | 'boolean', + required: boolean, + description?: string, + defaultValue?: string | number | boolean, + options?: (string | number)[], + min?: number, + max?: number + ) { + this.id = id + this.name = name + this.type = type + this.description = description + this.defaultValue = defaultValue + this.required = required + this.options = options + this.min = min + this.max = max + } +} + +const cockpitActionParametersInfo: Record = {} +export const cockpitActionParametersData: Record = {} + +export const getCockpitActionParametersInfo = (id: string): CockpitActionParameter | undefined => { + return cockpitActionParametersInfo[id] +} + +export const getAllCockpitActionParametersInfo = (): Record => { + return cockpitActionParametersInfo +} + +export const getCockpitActionParameterInfo = (id: string): CockpitActionParameter | undefined => { + return cockpitActionParametersInfo[id] +} + +export const setCockpitActionParameterInfo = (id: string, parameter: CockpitActionParameter): void => { + cockpitActionParametersInfo[id] = parameter +} + +export const getCockpitActionParameterData = (id: string): string | number | boolean | undefined => { + return cockpitActionParametersData[id] +} + +export const setCockpitActionParameterData = (id: string, data: string | number | boolean): void => { + cockpitActionParametersData[id] = data +} + +const fakeRovNameInfo = new CockpitActionParameter( + 'fakeRovName', + 'Placeholder Parameter One', + 'string', + true, + 'This is a placeholder parameter for the first parameter' +) + +const fakeRovAgeInitialDivesInfo = new CockpitActionParameter( + 'fakeRovAgeInitialDives', + 'Placeholder Parameter Two', + 'number', + true, + 'This is a placeholder parameter for the second parameter', + undefined, + undefined, + undefined, + 0 +) + +const placeholderParameterThreeInfo = new CockpitActionParameter( + 'placeholderParameterThree', + 'Placeholder Parameter Three', + 'boolean', + true, + 'This is a placeholder parameter for the third parameter' +) + +setCockpitActionParameterInfo(fakeRovNameInfo.id, fakeRovNameInfo) +setCockpitActionParameterInfo(fakeRovAgeInitialDivesInfo.id, fakeRovAgeInitialDivesInfo) +setCockpitActionParameterInfo(placeholderParameterThreeInfo.id, placeholderParameterThreeInfo) + +setCockpitActionParameterData(fakeRovNameInfo.id, 'John Doe') +setCockpitActionParameterData(fakeRovAgeInitialDivesInfo.id, 5) +setCockpitActionParameterData(placeholderParameterThreeInfo.id, true) diff --git a/src/libs/actions/http-request.ts b/src/libs/actions/http-request.ts new file mode 100644 index 000000000..80205448c --- /dev/null +++ b/src/libs/actions/http-request.ts @@ -0,0 +1,150 @@ +import { + CockpitAction, + CockpitActionsFunction, + registerActionCallback, + registerNewAction, +} from '../joystick/protocols/cockpit-actions' +import { getCockpitActionParameterData } from './data-lake' + +/** + * The types of HTTP methods that can be used. + */ +export enum HttpRequestMethod { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE', + PATCH = 'PATCH', +} +export const availableHttpRequestMethods: HttpRequestMethod[] = Object.values(HttpRequestMethod) + +export type HttpRequestActionConfig = { + /** + * The name of the action. + */ + name: string + /** + * The URL to send the request to. + */ + url: string + /** + * The HTTP method to use. + */ + method: HttpRequestMethod + /** + * The headers to send with the request. + */ + headers: Record + /** + * The URL parameters to send with the request. + */ + urlParams: Record + /** + * The body of the request. + */ + body: string +} + +let registeredHttpRequestActionConfigs: Record = {} + +export const registerHttpRequestActionConfig = (action: HttpRequestActionConfig): void => { + const id = `http-request-action (${action.name})` + registeredHttpRequestActionConfigs[id] = action + saveHttpRequestActionConfigs() + updateCockpitActions() +} + +export const getHttpRequestActionConfig = (id: string): HttpRequestActionConfig | undefined => { + return registeredHttpRequestActionConfigs[id] +} + +export const getAllHttpRequestActionConfigs = (): Record => { + return registeredHttpRequestActionConfigs +} + +export const deleteHttpRequestActionConfig = (id: string): void => { + delete registeredHttpRequestActionConfigs[id] + saveHttpRequestActionConfigs() + updateCockpitActions() +} + +export const updateHttpRequestActionConfig = (id: string, updatedAction: HttpRequestActionConfig): void => { + registeredHttpRequestActionConfigs[id] = updatedAction + saveHttpRequestActionConfigs() + updateCockpitActions() +} + +export const updateCockpitActions = (): void => { + const httpResquestActions = getAllHttpRequestActionConfigs() + for (const [id, action] of Object.entries(httpResquestActions)) { + try { + const cockpitAction = new CockpitAction(id as CockpitActionsFunction, action.name) + registerNewAction(cockpitAction) + registerActionCallback(cockpitAction, getHttpRequestActionCallback(id)) + } catch (error) { + console.error(`Error registering action ${id}: ${error}`) + } + } +} + +export const loadHttpRequestActionConfigs = (): void => { + const savedActions = localStorage.getItem('cockpit-http-request-actions') + if (savedActions) { + registeredHttpRequestActionConfigs = JSON.parse(savedActions) + } +} + +export const saveHttpRequestActionConfigs = (): void => { + localStorage.setItem('cockpit-http-request-actions', JSON.stringify(registeredHttpRequestActionConfigs)) +} + +export type HttpRequestActionCallback = () => void + +export const getHttpRequestActionCallback = (id: string): HttpRequestActionCallback => { + const action = getHttpRequestActionConfig(id) + if (!action) { + throw new Error(`Action with id ${id} not found.`) + } + + let parsedBody = action.body + const parsedUrlParams = action.urlParams + + const cockpitInputsInBody = action.body.match(/{{\s*([^{}\s]+)\s*}}/g) + if (cockpitInputsInBody) { + for (const input of cockpitInputsInBody) { + const parsedInput = input.replace('{{', '').replace('}}', '').trim() + const inputData = getCockpitActionParameterData(parsedInput) + if (inputData) { + parsedBody = parsedBody.replace(input, inputData.toString()) + } + } + } + + const cockpitInputsInUrlParams = Object.entries(action.urlParams).filter( + ([, value]) => typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}') + ) + if (cockpitInputsInUrlParams) { + for (const [key, value] of cockpitInputsInUrlParams) { + const parsedInput = value.replace('{{', '').replace('}}', '').trim() + const inputData = getCockpitActionParameterData(parsedInput) + if (inputData) { + parsedUrlParams[key] = inputData.toString() + } + } + } + + const url = new URL(action.url) + + url.search = new URLSearchParams(parsedUrlParams).toString() + + return () => { + fetch(url, { + method: action.method, + headers: action.headers, + body: action.method === HttpRequestMethod.GET ? undefined : parsedBody, + }) + } +} + +loadHttpRequestActionConfigs() +updateCockpitActions() diff --git a/src/libs/joystick/protocols.ts b/src/libs/joystick/protocols.ts index e24ae286f..0ef876ec1 100644 --- a/src/libs/joystick/protocols.ts +++ b/src/libs/joystick/protocols.ts @@ -7,14 +7,15 @@ import { } from './protocols/mavlink-manual-control' import { modifierKeyActions, otherAvailableActions } from './protocols/other' -export const allAvailableAxes: ProtocolAction[] = [ - ...Object.values(mavlinkManualControlAxes), - ...Object.values(otherAvailableActions), -] +export const allAvailableAxes = (): ProtocolAction[] => { + return [...Object.values(mavlinkManualControlAxes), ...Object.values(otherAvailableActions)] +} -export const allAvailableButtons: ProtocolAction[] = [ - ...Object.values(availableCockpitActions), - ...Object.values(availableMavlinkManualControlButtonFunctions), - ...Object.values(otherAvailableActions), - ...Object.values(modifierKeyActions), -] +export const allAvailableButtons = (): ProtocolAction[] => { + return [ + ...Object.values(availableCockpitActions), + ...Object.values(availableMavlinkManualControlButtonFunctions), + ...Object.values(otherAvailableActions), + ...Object.values(modifierKeyActions), + ] +} diff --git a/src/libs/joystick/protocols/cockpit-actions.ts b/src/libs/joystick/protocols/cockpit-actions.ts index 60fd2aa27..f76d54981 100644 --- a/src/libs/joystick/protocols/cockpit-actions.ts +++ b/src/libs/joystick/protocols/cockpit-actions.ts @@ -23,7 +23,7 @@ export enum CockpitActionsFunction { * An action to be performed by Cockpit itself */ export class CockpitAction implements ProtocolAction { - id: CockpitActionsFunction | string + id: CockpitActionsFunction name: string readonly protocol = JoystickProtocol.CockpitAction @@ -34,8 +34,8 @@ export class CockpitAction implements ProtocolAction { } } -// Available actions -export const availableCockpitActions: { [key in CockpitActionsFunction | string]: CockpitAction } = { +// Predefined actions +export const predefinedCockpitActions: { [key in CockpitActionsFunction]: CockpitAction } = { [CockpitActionsFunction.go_to_next_view]: new CockpitAction(CockpitActionsFunction.go_to_next_view, 'Go to next view'), [CockpitActionsFunction.go_to_previous_view]: new CockpitAction(CockpitActionsFunction.go_to_previous_view, 'Go to previous view'), [CockpitActionsFunction.toggle_full_screen]: new CockpitAction(CockpitActionsFunction.toggle_full_screen, 'Toggle full screen'), @@ -68,10 +68,11 @@ interface CallbackEntry { * Responsible for routing cockpit actions */ export class CockpitActionsManager { + availableActions: { [key in CockpitActionsFunction]: CockpitAction } = { ...predefinedCockpitActions } actionsCallbacks: Record = {} registerNewAction = (action: CockpitAction): void => { - availableCockpitActions[action.id] = action + this.availableActions[action.id] = action } registerActionCallback = (action: CockpitAction, callback: CockpitActionCallback): string => { @@ -101,6 +102,11 @@ export class CockpitActionsManager { export const cockpitActionsManager = new CockpitActionsManager() +export const registerNewAction = (action: CockpitAction): void => { + cockpitActionsManager.registerNewAction(action) + console.debug(`Registered new action ${action.name} with id (${action.id}).`) +} + export const registerActionCallback = (action: CockpitAction, callback: CockpitActionCallback): string => { return cockpitActionsManager.registerActionCallback(action, callback) } @@ -112,3 +118,5 @@ export const unregisterActionCallback = (id: string): void => { export const executeActionCallback = (id: string): void => { cockpitActionsManager.executeActionCallback(id) } + +export const availableCockpitActions = cockpitActionsManager.availableActions diff --git a/src/stores/controller.ts b/src/stores/controller.ts index 486955679..5e7de41cf 100644 --- a/src/stores/controller.ts +++ b/src/stores/controller.ts @@ -49,8 +49,8 @@ export const useControllerStore = defineStore('controller', () => { const protocolMappings = useBlueOsStorage(protocolMappingsKey, cockpitStandardToProtocols) const protocolMappingIndex = useBlueOsStorage(protocolMappingIndexKey, 0) const cockpitStdMappings = useBlueOsStorage(cockpitStdMappingsKey, availableGamepadToCockpitMaps) - const availableAxesActions = allAvailableAxes - const availableButtonActions = allAvailableButtons + const availableAxesActions = ref(allAvailableAxes()) + const availableButtonActions = ref(allAvailableButtons()) const enableForwarding = ref(false) const holdLastInputWhenWindowHidden = useBlueOsStorage('cockpit-hold-last-joystick-input-when-window-hidden', false) const vehicleTypeProtocolMappingCorrespondency = useBlueOsStorage( @@ -400,6 +400,11 @@ export const useControllerStore = defineStore('controller', () => { } }) + setInterval(() => { + availableButtonActions.value = allAvailableButtons() + availableAxesActions.value = allAvailableAxes() + }, 1000) + return { registerControllerUpdateCallback, enableForwarding, diff --git a/src/views/ConfigurationActionsView.vue b/src/views/ConfigurationActionsView.vue new file mode 100644 index 000000000..2c6ea6e5c --- /dev/null +++ b/src/views/ConfigurationActionsView.vue @@ -0,0 +1,576 @@ + + + + +