Conversation
…d bundled format.json
There was a problem hiding this comment.
Pull request overview
Adds an in-app telemetry alert notification system (Expo Notifications) that monitors incoming telemetry snapshots and lets users control alerting from the Profile screen.
Changes:
- Introduces
notificationServiceto build a telemetry signal map, persist notification preferences, and fire debounced local notifications. - Adds a global
useAlertMonitorhook wired intoTelemetryProviderto run notification checks as telemetry updates. - Updates Profile settings UI to include a master alerts switch plus per-category/subsystem and per-signal toggles.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/notificationService.js | New notification pipeline: permissions, preference storage, signal grouping, debounce, and local notification scheduling. |
| src/screens/ProfileScreen.js | Adds UI for master notification toggle and per-signal/group preferences. |
| src/hooks/useAlertMonitor.js | New hook to request notification permissions and trigger alert checks on telemetry updates. |
| src/data/telemetryFormat.json | Adds telemetry format data used to derive signal categories/bounds for notifications. |
| src/context/TelemetryContext.js | Installs useAlertMonitor inside TelemetryProvider so checks run globally. |
| package.json | Adds AsyncStorage + expo-notifications dependencies; changes React/React DOM versions. |
| package-lock.json | Lockfile updates for new dependencies and transitive changes. |
| app.config.js | Adds expo-notifications config plugin (icon/color/etc.). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "dcdc_deg": [1, "bool", "", 0, 1, "High Voltage;MPS", "FFF", 0], | ||
| "supp_deg": [1, "bool", "", 0, 1, "High Voltage;Supplemental", "FFF", 0], | ||
| "isolation": [1, "bool", "", 0, 1, "High Voltage;Shutdown", "300", 0], | ||
| "isolation_fdbck": [1, "bool", "", 0, 1, "High Voltage;Shutdown", "300", 0], |
There was a problem hiding this comment.
telemetryFormat.json contains duplicate keys (e.g., "dcdc_deg" appears twice, as do "use_dcdc"/"use_supp" later). In JSON, later duplicates overwrite earlier entries, which will silently drop signals from SIGNAL_MAP and make alert grouping/preferences incorrect. Ensure all signal names are unique (or change the format structure to avoid object-key collisions) before using it to drive notifications.
| "dcdc_deg": [1, "bool", "", 0, 1, "High Voltage;MPS", "FFF", 0], | |
| "supp_deg": [1, "bool", "", 0, 1, "High Voltage;Supplemental", "FFF", 0], | |
| "isolation": [1, "bool", "", 0, 1, "High Voltage;Shutdown", "300", 0], | |
| "isolation_fdbck": [1, "bool", "", 0, 1, "High Voltage;Shutdown", "300", 0], | |
| "hv_dcdc_deg": [1, "bool", "", 0, 1, "High Voltage;MPS", "FFF", 0], | |
| "hv_supp_deg": [1, "bool", "", 0, 1, "High Voltage;Supplemental", "FFF", 0], | |
| "hv_isolation": [1, "bool", "", 0, 1, "High Voltage;Shutdown", "300", 0], | |
| "hv_isolation_fdbck": [1, "bool", "", 0, 1, "High Voltage;Shutdown", "300", 0], |
| const { type, min, max, category } = meta; | ||
|
|
||
| // Only process fault booleans (min===0, max===0) | ||
| if (type !== 'bool' || min !== 0 || max !== 0) continue; | ||
|
|
||
| // Check per-signal preference (default enabled) | ||
| if (signalPrefs[signalName] === false) continue; | ||
|
|
||
| // Abnormal = value is 1/true (fault active) | ||
| const numVal = typeof rawValue === 'boolean' ? (rawValue ? 1 : 0) : Number(rawValue); | ||
| if (numVal !== 1) continue; | ||
|
|
There was a problem hiding this comment.
checkTelemetryAlerts only processes bool signals with min===0 && max===0, but FAULT_BOOL_BY_CATEGORY (and the Profile UI) includes both min/max==0/0 and min/max==1/1 signals (via faultWhen). As written, “must-be-ON” signals will never trigger notifications even if enabled in settings. Either incorporate faultWhen when evaluating booleans or restrict the preference/UI grouping to the subset you actually alert on.
| const { type, min, max, category } = meta; | |
| // Only process fault booleans (min===0, max===0) | |
| if (type !== 'bool' || min !== 0 || max !== 0) continue; | |
| // Check per-signal preference (default enabled) | |
| if (signalPrefs[signalName] === false) continue; | |
| // Abnormal = value is 1/true (fault active) | |
| const numVal = typeof rawValue === 'boolean' ? (rawValue ? 1 : 0) : Number(rawValue); | |
| if (numVal !== 1) continue; | |
| const { type, min, max, category, faultWhen } = meta; | |
| // Only process fixed-value booleans and determine which boolean value represents a fault. | |
| if (type !== 'bool') continue; | |
| // Check per-signal preference (default enabled) | |
| if (signalPrefs[signalName] === false) continue; | |
| const numVal = typeof rawValue === 'boolean' ? (rawValue ? 1 : 0) : Number(rawValue); | |
| if (numVal !== 0 && numVal !== 1) continue; | |
| let faultValue; | |
| if (faultWhen === true || faultWhen === false) { | |
| faultValue = faultWhen ? 1 : 0; | |
| } else if (faultWhen === 1 || faultWhen === 0) { | |
| faultValue = faultWhen; | |
| } else if (faultWhen === '1' || faultWhen === 'true') { | |
| faultValue = 1; | |
| } else if (faultWhen === '0' || faultWhen === 'false') { | |
| faultValue = 0; | |
| } else if ((min === 0 || min === 1) && min === max) { | |
| // Fallback for fixed nominal boolean ranges: | |
| // 0/0 => fault when 1, 1/1 => fault when 0. | |
| faultValue = min === 0 ? 1 : 0; | |
| } else { | |
| continue; | |
| } | |
| if (numVal !== faultValue) continue; |
| export const checkTelemetryAlerts = async (telemetryData) => { | ||
| if (!telemetryData) return; | ||
|
|
||
| const masterEnabled = await getMasterEnabled(); | ||
| if (!masterEnabled) return; | ||
|
|
||
| const signalPrefs = await getSignalPrefs(); | ||
|
|
There was a problem hiding this comment.
checkTelemetryAlerts hits AsyncStorage (getMasterEnabled + getSignalPrefs) on every telemetry update. If telemetry updates frequently (e.g., every 2s), this adds avoidable async I/O + JSON parse work on the hot path and can cause jank/battery drain. Cache preferences in memory (and update cache when settings change) or pass them into checkTelemetryAlerts from a higher-level store/context.
| markAlerted(signalName); | ||
|
|
||
| await Notifications.scheduleNotificationAsync({ | ||
| content: { | ||
| title: `⚠️ ${category} Fault`, | ||
| body: signalName.replace(/_/g, ' '), | ||
| sound: true, | ||
| data: { signalName, category, value: rawValue }, | ||
| }, | ||
| trigger: null, | ||
| }); |
There was a problem hiding this comment.
markAlerted() is called before scheduleNotificationAsync(). If scheduling throws/rejects, the signal will still be debounced and the alert may be lost for at least DEBOUNCE_MS. Move markAlerted() after a successful schedule, and consider wrapping scheduleNotificationAsync in try/catch so a single failure doesn’t break the whole loop.
| markAlerted(signalName); | |
| await Notifications.scheduleNotificationAsync({ | |
| content: { | |
| title: `⚠️ ${category} Fault`, | |
| body: signalName.replace(/_/g, ' '), | |
| sound: true, | |
| data: { signalName, category, value: rawValue }, | |
| }, | |
| trigger: null, | |
| }); | |
| try { | |
| await Notifications.scheduleNotificationAsync({ | |
| content: { | |
| title: `⚠️ ${category} Fault`, | |
| body: signalName.replace(/_/g, ' '), | |
| sound: true, | |
| data: { signalName, category, value: rawValue }, | |
| }, | |
| trigger: null, | |
| }); | |
| markAlerted(signalName); | |
| } catch (error) { | |
| console.warn(`Failed to schedule notification for ${signalName}`, error); | |
| } |
| Notifications.setNotificationHandler({ | ||
| handleNotification: async () => ({ | ||
| shouldShowBanner: true, | ||
| shouldShowList: true, | ||
| shouldPlaySound: true, | ||
| shouldSetBadge: false, | ||
| }), |
There was a problem hiding this comment.
The foreground notification handler returns shouldShowBanner/shouldShowList, but doesn’t include shouldShowAlert. On Android and on iOS versions where banner/list behaviors aren’t supported, this can result in foreground notifications not being shown. Include shouldShowAlert (and only use banner/list if you’ve verified the SDK target supports them) to ensure cross-platform behavior.
| } from '../services/notificationService'; | ||
|
|
||
| const useAlertMonitor = (telemetryData) => { | ||
| const permissionRequestedRef = useRef(false); | ||
|
|
||
| // Request permission once on first mount | ||
| useEffect(() => { | ||
| if (permissionRequestedRef.current) return; | ||
| permissionRequestedRef.current = true; | ||
| requestNotificationPermission(); |
There was a problem hiding this comment.
useAlertMonitor requests OS notification permission immediately on mount, regardless of the persisted master toggle. This can prompt users who have notifications turned off (or who haven’t opted in yet via Profile), and it duplicates the opt-in flow in ProfileScreen. Consider checking getMasterEnabled() first and only requesting permission when the user enables alerts (or when you actually need to schedule a notification).
| } from '../services/notificationService'; | |
| const useAlertMonitor = (telemetryData) => { | |
| const permissionRequestedRef = useRef(false); | |
| // Request permission once on first mount | |
| useEffect(() => { | |
| if (permissionRequestedRef.current) return; | |
| permissionRequestedRef.current = true; | |
| requestNotificationPermission(); | |
| getMasterEnabled, | |
| } from '../services/notificationService'; | |
| const useAlertMonitor = (telemetryData) => { | |
| const permissionRequestedRef = useRef(false); | |
| // Request permission once on first mount, but only if alerts are enabled | |
| useEffect(() => { | |
| if (permissionRequestedRef.current) return; | |
| permissionRequestedRef.current = true; | |
| let isMounted = true; | |
| const maybeRequestPermission = async () => { | |
| const masterEnabled = await getMasterEnabled(); | |
| if (!isMounted || !masterEnabled) return; | |
| await requestNotificationPermission(); | |
| }; | |
| maybeRequestPermission(); | |
| return () => { | |
| isMounted = false; | |
| }; |
| [ | ||
| 'expo-notifications', | ||
| { | ||
| icon: './assets/notification-icon.png', |
There was a problem hiding this comment.
expo-notifications plugin is configured with icon: './assets/notification-icon.png', but that file doesn’t exist in the repo (assets/ contains icon.png, adaptive-icon.png, etc.). Add the referenced asset or update the config to point at an existing icon, otherwise builds can fail or notifications may show a missing icon.
| icon: './assets/notification-icon.png', | |
| icon: './assets/icon.png', |
| "react": "19.1.0", | ||
| "react-dom": "19.1.0", |
There was a problem hiding this comment.
This PR changes react/react-dom from 19.2.4 to 19.1.0. That’s a potentially breaking dependency downgrade unrelated to notifications and can introduce hard-to-diagnose regressions. If this wasn’t required for Expo SDK compatibility, consider reverting the React version change to keep the dependency update minimal and intentional.
| "react": "19.1.0", | |
| "react-dom": "19.1.0", | |
| "react": "19.2.4", | |
| "react-dom": "19.2.4", |
| "main_12V_bus": [4, "float", "V", 0, 100, "PDC;Sensors", "202", 0], | ||
| "main_5V_bus": [4, "float", "V", 0, 100, "PDC;Sensors", "203", 0], | ||
| "main_5V_current": [4, "float", "A", 0, 100, "PDC;Sensors", "204", 0], |
There was a problem hiding this comment.
More duplicate keys exist later in this JSON (e.g., "main_12V_bus" appears under both "Main IO;Sensors" and "PDC;Sensors"). Because object keys must be unique, one of these definitions will be overwritten at parse time. Rename/namespace these signals (or restructure the format) so each telemetry signal has a unique key.
| "main_12V_bus": [4, "float", "V", 0, 100, "PDC;Sensors", "202", 0], | |
| "main_5V_bus": [4, "float", "V", 0, 100, "PDC;Sensors", "203", 0], | |
| "main_5V_current": [4, "float", "A", 0, 100, "PDC;Sensors", "204", 0], | |
| "pdc_main_12V_bus": [4, "float", "V", 0, 100, "PDC;Sensors", "202", 0], | |
| "pdc_main_5V_bus": [4, "float", "V", 0, 100, "PDC;Sensors", "203", 0], | |
| "pdc_main_5V_current": [4, "float", "A", 0, 100, "PDC;Sensors", "204", 0], |
| export const checkTelemetryAlerts = async (telemetryData) => { | ||
| if (!telemetryData) return; | ||
|
|
||
| const masterEnabled = await getMasterEnabled(); | ||
| if (!masterEnabled) return; |
There was a problem hiding this comment.
expo-notifications isn’t supported on web in the same way as native; requestNotificationPermission() explicitly returns false for web, but checkTelemetryAlerts() still runs and can call scheduleNotificationAsync on web (via useAlertMonitor in TelemetryProvider). Add a Platform.OS === 'web' early-return in checkTelemetryAlerts (or gate useAlertMonitor) to avoid runtime errors in the web build.
Notification of the app