From 88606055374da79cbb4872ff13d2023afbd826d7 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Tue, 9 Dec 2025 10:56:18 -0300 Subject: [PATCH 1/4] blueos: Segregate definition of data-lake variables --- src/libs/blueos/definitions.ts | 21 +++++++++++++++++++++ src/stores/mainVehicle.ts | 12 +----------- 2 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 src/libs/blueos/definitions.ts diff --git a/src/libs/blueos/definitions.ts b/src/libs/blueos/definitions.ts new file mode 100644 index 0000000000..6288ca7fd1 --- /dev/null +++ b/src/libs/blueos/definitions.ts @@ -0,0 +1,21 @@ +// Register BlueOS variables in the data lake +export const blueOsVariables = { + cpuTemp: { + id: 'blueos/cpu/tempC', + name: 'BlueOS CPU Temperature', + type: 'number', + description: 'The average temperature of the BlueOS CPU cores in °C.', + }, + cpuUsageAverage: { + id: 'blueos/cpu/usageAverage', + name: 'BlueOS CPU Usage', + type: 'number', + description: 'The average usage of the BlueOS CPU cores in %.', + }, + cpuFrequencyAverage: { + id: 'blueos/cpu/frequencyAverage', + name: 'BlueOS CPU Frequency', + type: 'number', + description: 'The average frequency of the BlueOS CPU cores in Hz.', + }, +} diff --git a/src/stores/mainVehicle.ts b/src/stores/mainVehicle.ts index ef8116ff1e..2628a0c819 100644 --- a/src/stores/mainVehicle.ts +++ b/src/stores/mainVehicle.ts @@ -20,6 +20,7 @@ import { getVehicleName, setKeyDataOnCockpitVehicleStorage, } from '@/libs/blueos' +import { blueOsVariables } from '@/libs/blueos/definitions' import * as Connection from '@/libs/connection/connection' import { ConnectionManager } from '@/libs/connection/connection-manager' import type { Package } from '@/libs/connection/m2r/messages/mavlink2rest' @@ -624,17 +625,6 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { updateVehicleId() - // Register BlueOS variables in the data lake - const blueOsVariables = { - cpuTemp: { id: 'blueos/cpu/tempC', name: 'CPU Temperature', type: 'number' }, - cpuUsageAverage: { id: 'blueos/cpu/usageAverage', name: 'BlueOS CPU Usage (average)', type: 'number' }, - cpuFrequencyAverage: { - id: 'blueos/cpu/frequencyAverage', - name: 'BlueOS CPU Frequency (average)', - type: 'number', - }, - } - const cpuUsageVariableId = (cpuName: string): string => `blueos/${cpuName}/usage` const cpuFrequencyVariableId = (cpuName: string): string => `blueos/${cpuName}/frequency` From ea639d6380401a596a4f8c95c30cda8439024cc8 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Tue, 9 Dec 2025 10:57:23 -0300 Subject: [PATCH 2/4] alerts: Create a pure `ts` manager to replace the `Pinia` store --- src/libs/alert-manager.ts | 154 ++++++++++++++++++++++++++++++++++++++ src/stores/alert.ts | 98 +++++++++++++----------- 2 files changed, 207 insertions(+), 45 deletions(-) create mode 100644 src/libs/alert-manager.ts diff --git a/src/libs/alert-manager.ts b/src/libs/alert-manager.ts new file mode 100644 index 0000000000..916295c67c --- /dev/null +++ b/src/libs/alert-manager.ts @@ -0,0 +1,154 @@ +import { v4 as uuid } from 'uuid' + +import { Alert, AlertLevel } from '@/types/alert' + +/** + * Callback type for alert listeners + */ +type AlertListener = (alert: Alert, index: number) => void + +/** + * Internal state for the alert manager + */ +const alerts: Alert[] = [] +const alertListeners: Record = {} + +/** + * Get all alerts + * @returns {Alert[]} All alerts + */ +export const getAllAlerts = (): Alert[] => { + return [...alerts] +} + +/** + * Get a sorted copy of all alerts by time created + * @returns {Alert[]} Sorted alerts + */ +export const getSortedAlerts = (): Alert[] => { + return [...alerts].sort((a, b) => a.time_created.getTime() - b.time_created.getTime()) +} + +/** + * Push a new alert + * @param {Alert} alert - The alert to push + */ +export const pushAlert = (alert: Alert): void => { + alerts.push(alert) + + // Log to console based on level + switch (alert.level) { + case AlertLevel.Success: + console.log(alert.message) + break + case AlertLevel.Error: + console.error(alert.message) + break + case AlertLevel.Info: + console.info(alert.message) + break + case AlertLevel.Warning: + console.warn(alert.message) + break + case AlertLevel.Critical: + console.error(alert.message) + break + default: + console.warn(`Unknown alert level. Message: ${alert.message}`) + break + } + + // Notify listeners + const alertIndex = alerts.length - 1 + Object.values(alertListeners).forEach((listener) => { + try { + listener(alert, alertIndex) + } catch (error) { + console.error('Error in alert listener:', error) + } + }) +} + +/** + * Push a success alert + * @param {string} message - The alert message + * @param {Date} timeCreated - Optional time created + */ +export const pushSuccessAlert = (message: string, timeCreated: Date = new Date()): void => { + pushAlert(new Alert(AlertLevel.Success, message, timeCreated)) +} + +/** + * Push an error alert + * @param {string} message - The alert message + * @param {Date} timeCreated - Optional time created + */ +export const pushErrorAlert = (message: string, timeCreated: Date = new Date()): void => { + pushAlert(new Alert(AlertLevel.Error, message, timeCreated)) +} + +/** + * Push an info alert + * @param {string} message - The alert message + * @param {Date} timeCreated - Optional time created + */ +export const pushInfoAlert = (message: string, timeCreated: Date = new Date()): void => { + pushAlert(new Alert(AlertLevel.Info, message, timeCreated)) +} + +/** + * Push a warning alert + * @param {string} message - The alert message + * @param {Date} timeCreated - Optional time created + */ +export const pushWarningAlert = (message: string, timeCreated: Date = new Date()): void => { + pushAlert(new Alert(AlertLevel.Warning, message, timeCreated)) +} + +/** + * Push a critical alert + * @param {string} message - The alert message + * @param {Date} timeCreated - Optional time created + */ +export const pushCriticalAlert = (message: string, timeCreated: Date = new Date()): void => { + pushAlert(new Alert(AlertLevel.Critical, message, timeCreated)) +} + +/** + * Subscribe to alert events + * @param {AlertListener} listener - The listener function + * @returns {string} The listener ID (used for unsubscribing) + */ +export const subscribeToAlerts = (listener: AlertListener): string => { + const listenerId = uuid() + alertListeners[listenerId] = listener + return listenerId +} + +/** + * Unsubscribe from alert events + * @param {string} listenerId - The listener ID to unsubscribe + */ +export const unsubscribeFromAlerts = (listenerId: string): void => { + delete alertListeners[listenerId] +} + +/** + * Get the current number of alerts + * @returns {number} The number of alerts + */ +export const getAlertsCount = (): number => { + return alerts.length +} + +/** + * Get an alert by index + * @param {number} index - The alert index + * @returns {Alert | undefined} The alert if found + */ +export const getAlertByIndex = (index: number): Alert | undefined => { + return alerts[index] +} + +// Push initial alert +pushAlert(new Alert(AlertLevel.Success, 'Cockpit started')) diff --git a/src/stores/alert.ts b/src/stores/alert.ts index 31ea8a371c..ba62da6388 100644 --- a/src/stores/alert.ts +++ b/src/stores/alert.ts @@ -1,17 +1,30 @@ import { defineStore } from 'pinia' -import { computed, reactive, ref, watch } from 'vue' +import { computed, onUnmounted, reactive, ref } from 'vue' import { useBlueOsStorage } from '@/composables/settingsSyncer' +import { + getAlertsCount, + getAllAlerts, + pushAlert as managerPushAlert, + pushCriticalAlert as managerPushCriticalAlert, + pushErrorAlert as managerPushErrorAlert, + pushInfoAlert as managerPushInfoAlert, + pushSuccessAlert as managerPushSuccessAlert, + pushWarningAlert as managerPushWarningAlert, + subscribeToAlerts, + unsubscribeFromAlerts, +} from '@/libs/alert-manager' import { Alert, AlertLevel } from '../types/alert' export const useAlertStore = defineStore('alert', () => { - const alerts = reactive([new Alert(AlertLevel.Success, 'Cockpit started')]) + // Reactive alerts array that syncs with the manager + const alerts = reactive(getAllAlerts()) + + // Settings stored in BlueOS storage (Vue composable) const enableVoiceAlerts = useBlueOsStorage('cockpit-enable-voice-alerts', true) const neverShowArmedMenuWarning = useBlueOsStorage('cockpit-never-show-armed-menu-warning', false) const skipArmedMenuWarningThisSession = ref(false) - // eslint-disable-next-line jsdoc/require-jsdoc - const availableAlertSpeechVoices = reactive([]) const selectedAlertSpeechVoiceName = useBlueOsStorage( 'cockpit-selected-alert-speech-voice', undefined @@ -25,53 +38,56 @@ export const useAlertStore = defineStore('alert', () => { ]) const alertVolume = useBlueOsStorage('cockpit-alert-volume', 1) + // Speech synthesis state + // eslint-disable-next-line jsdoc/require-jsdoc + const availableAlertSpeechVoices = reactive([]) + const lastSpokenAlertIndex = ref(0) + const sortedAlerts = computed(() => { - return alerts.sort((a, b) => a.time_created.getTime() - b.time_created.getTime()) + return [...alerts].sort((a, b) => a.time_created.getTime() - b.time_created.getTime()) }) + // Wrapper functions that delegate to the manager const pushAlert = (alert: Alert): void => { - alerts.push(alert) - - switch (alert.level) { - case AlertLevel.Success: - console.log(alert.message) - break - case AlertLevel.Error: - console.error(alert.message) - break - case AlertLevel.Info: - console.info(alert.message) - break - case AlertLevel.Warning: - console.warn(alert.message) - break - case AlertLevel.Critical: - console.error(alert.message) - break - default: - unimplemented(`A new alert level was added but we have not updated - this part of the code. Regardless of that, here's the alert message: ${alert.message}`) - break - } + managerPushAlert(alert) } const pushSuccessAlert = (message: string, time_created: Date = new Date()): void => { - pushAlert(new Alert(AlertLevel.Success, message, time_created)) + managerPushSuccessAlert(message, time_created) } + const pushErrorAlert = (message: string, time_created: Date = new Date()): void => { - pushAlert(new Alert(AlertLevel.Error, message, time_created)) + managerPushErrorAlert(message, time_created) } + const pushInfoAlert = (message: string, time_created: Date = new Date()): void => { - pushAlert(new Alert(AlertLevel.Info, message, time_created)) + managerPushInfoAlert(message, time_created) } + const pushWarningAlert = (message: string, time_created: Date = new Date()): void => { - pushAlert(new Alert(AlertLevel.Warning, message, time_created)) + managerPushWarningAlert(message, time_created) } + const pushCriticalAlert = (message: string, time_created: Date = new Date()): void => { - pushAlert(new Alert(AlertLevel.Critical, message, time_created)) + managerPushCriticalAlert(message, time_created) } - // Alert speech syntesis routine + // Subscribe to manager alerts to keep reactive array in sync and handle speech + const alertListenerId = subscribeToAlerts((alert, alertIndex) => { + // Keep reactive array in sync + if (alerts.length < getAlertsCount()) { + alerts.push(alert) + } + + // Handle speech synthesis + const alertLevelEnabled = enabledAlertLevels.value.find((enabledAlert) => enabledAlert.level === alert.level) + const shouldMute = + !enableVoiceAlerts.value || + ((alertLevelEnabled === undefined || !alertLevelEnabled.enabled) && !alert.message.startsWith('#')) + speak(alert.message, alertIndex, shouldMute) + }) + + // Alert speech synthesis routine const synth = window.speechSynthesis // We need to cache these otherwise they get garbage collected... @@ -113,9 +129,6 @@ export const useAlertStore = defineStore('alert', () => { availableAlertSpeechVoices.map((v) => ({ value: v.name, name: `${v.name} (${v.lang})` })) ) - // Track the index of the last alert that finished being spoken - const lastSpokenAlertIndex = ref(0) - /** * Speaks a text out loud using the browsers TTS engine * @param {string} text - The text to speak @@ -147,14 +160,9 @@ export const useAlertStore = defineStore('alert', () => { synth.speak(utterance) } - watch(alerts, () => { - const lastAlertIndex = alerts.length - 1 - const lastAlert = alerts[lastAlertIndex] - const alertLevelEnabled = enabledAlertLevels.value.find((enabledAlert) => enabledAlert.level === lastAlert.level) - const shouldMute = - !enableVoiceAlerts.value || - ((alertLevelEnabled === undefined || !alertLevelEnabled.enabled) && !lastAlert.message.startsWith('#')) - speak(lastAlert.message, lastAlertIndex, shouldMute) + // Cleanup subscription when store is unmounted + onUnmounted(() => { + unsubscribeFromAlerts(alertListenerId) }) return { From 61683e03b1c3ff2ae085dcc2d9207d28d33ba54a Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Tue, 9 Dec 2025 10:59:04 -0300 Subject: [PATCH 3/4] alerts: Improve the UI of the config menu --- src/views/ConfigurationAlertsView.vue | 40 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/views/ConfigurationAlertsView.vue b/src/views/ConfigurationAlertsView.vue index a8d8151977..6934ee5eb2 100644 --- a/src/views/ConfigurationAlertsView.vue +++ b/src/views/ConfigurationAlertsView.vue @@ -4,17 +4,33 @@