diff --git a/src/actions/index.js b/src/actions/index.js index a44d3bfe3..a3d630acb 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -7,7 +7,7 @@ import MyCommaAuth from '@commaai/my-comma-auth'; import * as Types from './types'; import { resetPlayback, selectLoop } from '../timeline/playback'; import {hasRoutesData } from '../timeline/segments'; -import { getDeviceFromState, deviceVersionAtLeast } from '../utils'; +import { getDeviceFromState, deviceVersionAtLeast, deviceIsOnline } from '../utils'; let routesRequest = null; let routesRequestPromise = null; @@ -415,6 +415,35 @@ export function fetchDeviceNetworkStatus(dongleId) { }; } +export function fetchDeviceNotCar(dongleId) { + return async (dispatch, getState) => { + const device = getDeviceFromState(getState(), dongleId); + if (!deviceIsOnline(device)) { + return; + } + const payload = { + id: 0, + jsonrpc: '2.0', + method: 'getNotCar', + }; + try { + const resp = await Athena.postJsonRpcPayload(dongleId, payload); + if (resp && resp.result !== undefined) { + dispatch({ + type: Types.ACTION_UPDATE_DEVICE_RPC, + dongleId, + fields: { not_car: resp.result === true }, + }); + } + } catch (err) { + if (!err.message || err.message.indexOf('Device not registered') === -1) { + console.error(err); + Sentry.captureException(err, { fingerprint: 'athena_fetch_notcar' }); + } + } + }; +} + export function updateDevices(devices) { return { type: Types.ACTION_UPDATE_DEVICES, diff --git a/src/actions/types.js b/src/actions/types.js index b9dc4aa11..06a4490e1 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -16,6 +16,7 @@ export const ACTION_UPDATE_ROUTE_LOCATION = 'ACTION_UPDATE_ROUTE_LOCATION'; export const ACTION_UPDATE_SHARED_DEVICE = 'ACTION_UPDATE_SHARED_DEVICE'; export const ACTION_UPDATE_DEVICE_ONLINE = 'ACTION_UPDATE_DEVICE_ONLINE'; export const ACTION_UPDATE_DEVICE_NETWORK = 'ACTION_UPDATE_DEVICE_NETWORK'; +export const ACTION_UPDATE_DEVICE_RPC = 'ACTION_UPDATE_DEVICE_RPC'; export const ACTION_PRIME_NAV = 'ACTION_PRIME_NAV'; export const ACTION_PRIME_SUBSCRIPTION = 'ACTION_PRIME_SUBSCRIPTION'; export const ACTION_PRIME_SUBSCRIBE_INFO = 'ACTION_PRIME_SUBSCRIBE_INFO'; diff --git a/src/components/DeviceInfo/index.jsx b/src/components/DeviceInfo/index.jsx index 9ed3ca4e7..9dda12c56 100644 --- a/src/components/DeviceInfo/index.jsx +++ b/src/components/DeviceInfo/index.jsx @@ -8,7 +8,7 @@ import { withStyles, Typography, Button, CircularProgress, Popper, Tooltip } fro import AccessTime from '@material-ui/icons/AccessTime'; import { athena as Athena, devices as Devices } from '@commaai/api'; -import { analyticsEvent, primeNav } from '../../actions'; +import { analyticsEvent, primeNav, fetchDeviceNotCar } from '../../actions'; import Colors from '../../colors'; import { GamepadIcon } from '../../icons'; import { deviceNamePretty, deviceIsOnline, deviceVersionAtLeast } from '../../utils'; @@ -207,7 +207,6 @@ class DeviceInfo extends Component { snapshot: {}, windowWidth: window.innerWidth, isTimeSelectOpen: false, - isCommaBody: false, bodyTeleopOpen: false, }; @@ -217,7 +216,6 @@ class DeviceInfo extends Component { this.onVisible = this.onVisible.bind(this); this.fetchDeviceInfo = this.fetchDeviceInfo.bind(this); this.fetchDeviceCarHealth = this.fetchDeviceCarHealth.bind(this); - this.fetchIsNotCar = this.fetchIsNotCar.bind(this); this.takeSnapshot = this.takeSnapshot.bind(this); this.snapshotType = this.snapshotType.bind(this); this.renderButtons = this.renderButtons.bind(this); @@ -250,7 +248,6 @@ class DeviceInfo extends Component { carHealth: {}, snapshot: {}, windowWidth: window.innerWidth, - isCommaBody: false, }); } } @@ -264,11 +261,11 @@ class DeviceInfo extends Component { } onVisible() { - const { device } = this.props; + const { device, dongleId } = this.props; if (!device.shared) { this.fetchDeviceInfo(); this.fetchDeviceCarHealth(); - this.fetchIsNotCar(); + this.props.dispatch(fetchDeviceNotCar(dongleId)); } } @@ -320,29 +317,6 @@ class DeviceInfo extends Component { } } - async fetchIsNotCar() { - const { dongleId, device } = this.props; - if (!deviceIsOnline(device)) { - return; - } - - try { - const payload = { - method: 'getNotCar', - jsonrpc: '2.0', - id: 0, - }; - const resp = await Athena.postJsonRpcPayload(dongleId, payload); - if (this.mounted && dongleId === this.props.dongleId) { - this.setState({ isCommaBody: resp.result === true }); - } - } catch (err) { - if (!err.message || err.message.indexOf('Device not registered') === -1) { - console.error(err); - } - } - } - async takeSnapshot() { const { dongleId } = this.props; const { snapshot } = this.state; @@ -518,7 +492,8 @@ class DeviceInfo extends Component { renderButtons() { const { classes, device } = this.props; - const { snapshot, carHealth, windowWidth, isTimeSelectOpen, isCommaBody } = this.state; + const { snapshot, carHealth, windowWidth, isTimeSelectOpen } = this.state; + const isCommaBody = device?.rpc?.not_car; let batteryVoltage; let batteryBackground = Colors.grey400; diff --git a/src/reducers/globalState.js b/src/reducers/globalState.js index e0a394c38..4e74fd415 100644 --- a/src/reducers/globalState.js +++ b/src/reducers/globalState.js @@ -107,6 +107,11 @@ export default function reducer(_state, action) { state = { ...state, devices: action.devices + .map((d) => { + // `rpc` holds Athena RPC-fetched values that would be wiped by listDevices payload + const prev = (_state.devices || []).find((p) => p.dongle_id === d.dongle_id); + return prev && prev.rpc ? { ...d, rpc: prev.rpc } : d; + }) .map(populateFetchedAt) .sort(deviceCompareFn), }; @@ -251,6 +256,34 @@ export default function reducer(_state, action) { }; } break; + case Types.ACTION_UPDATE_DEVICE_RPC: + // merge RPC-fetched values (e.g. not_car) into a specific device's `rpc` field + state = { + ...state, + devices: [...state.devices], + }; + deviceIndex = state.devices.findIndex((d) => d.dongle_id === action.dongleId); + + if (deviceIndex !== -1) { + state.devices[deviceIndex] = { + ...state.devices[deviceIndex], + rpc: { + ...state.devices[deviceIndex].rpc, + ...action.fields, + }, + }; + } + + if (state.device.dongle_id === action.dongleId) { + state.device = { + ...state.device, + rpc: { + ...state.device.rpc, + ...action.fields, + }, + }; + } + break; case Types.ACTION_PRIME_NAV: state = { ...state,