diff --git a/Changelog.md b/Changelog.md index c343e093..d509a0b4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,7 @@ ### Added +- Digital channels can now be configured as triggers in the `Scope` pane. - Option for cooshing a trigger bias level. - Option for choosing the triggering edge. diff --git a/src/actions/deviceActions.ts b/src/actions/deviceActions.ts index 235fc9dc..cd3928a3 100644 --- a/src/actions/deviceActions.ts +++ b/src/actions/deviceActions.ts @@ -64,19 +64,24 @@ import { import { updateGainsAction } from '../slices/gainsSlice'; import { clearProgress, + DigitalChannelTriggerStatesEnum, getTriggerBias, getTriggerRecordingLength, resetTriggerOrigin, setProgress, setTriggerActive, setTriggerOrigin, + TriggerEdge, } from '../slices/triggerSlice'; import { updateRegulator as updateRegulatorAction } from '../slices/voltageRegulatorSlice'; import { convertBits16 } from '../utils/bitConversion'; import { convertTimeToSeconds } from '../utils/duration'; import { isDiskFull } from '../utils/fileUtils'; import { isDataLoggerPane } from '../utils/panes'; -import { setSpikeFilter as persistSpikeFilter } from '../utils/persistentStore'; +import { + digitalChannelStateTupleOf8, + setSpikeFilter as persistSpikeFilter, +} from '../utils/persistentStore'; let device: null | SerialDevice = null; let updateRequestInterval: NodeJS.Timeout | undefined; @@ -210,6 +215,68 @@ const initGains = (): AppThunk> => async dispatch => { ); }; +function checkDigitalTriggerValidity( + unsignedBits: number, + previousUnsignedBits: number, + channelTriggerStatuses: digitalChannelStateTupleOf8 +): boolean { + const channelTriggerStatusesReversed = [ + ...channelTriggerStatuses, + ].reverse(); + + const doNotCareMask = Number.parseInt( + channelTriggerStatusesReversed + .map(status => + status === DigitalChannelTriggerStatesEnum.DoNotCare ? '0' : '1' + ) + .join(''), + 2 + ); + + const validMask = Number.parseInt( + channelTriggerStatusesReversed + .map(status => + status === DigitalChannelTriggerStatesEnum.DoNotCare + ? '0' + : status + ) + .join(''), + 2 + ); + + const isTriggerValid = (bits: number) => + ((bits & doNotCareMask) ^ validMask) === 0; + + return ( + !isTriggerValid(previousUnsignedBits) && isTriggerValid(unsignedBits) + ); +} + +function checkAnalogTriggerValidity( + cappedValue: number, + prevCappedValue: number | undefined, + triggerLevel: number, + triggerEdge: TriggerEdge +): boolean { + const isRaisingEdge = triggerEdge === 'Raising Edge'; + const isLoweringEdge = triggerEdge === 'Lowering Edge'; + + let validTriggerValue = false; + + if (isRaisingEdge) { + validTriggerValue = + prevCappedValue != null && + prevCappedValue < triggerLevel && + cappedValue >= triggerLevel; + } else if (isLoweringEdge) { + validTriggerValue = + prevCappedValue != null && + prevCappedValue > triggerLevel && + cappedValue <= triggerLevel; + } + return validTriggerValue; +} + export const open = (deviceInfo: Device): AppThunk> => async (dispatch, getState) => { @@ -223,6 +290,7 @@ export const open = let prevValue = 0; let prevCappedValue: number | undefined; let prevBits = 0; + let prevUnsignedBits = 0; let nbSamples = 0; let nbSamplesTotal = 0; @@ -243,6 +311,9 @@ export const open = cappedValue = 0; } + const channelTriggerStatuses = + state.app.trigger.digitalChannelsTriggersStates; + const unsignedBits = bits !== undefined ? bits & 0xff : 0; const b16 = convertBits16(bits!); if (samplingRunning && sampleFreq < maxSampleFreq) { @@ -267,25 +338,25 @@ export const open = prevBits = 0; if (getRecordingMode(state) === 'Scope') { - const isRaisingEdge = state.app.trigger.edge === 'Raising Edge'; - const isLoweringEdge = - state.app.trigger.edge === 'Lowering Edge'; - - let validTriggerValue = false; - - if (isRaisingEdge) { - validTriggerValue = - prevCappedValue != null && - prevCappedValue < state.app.trigger.level && - cappedValue >= state.app.trigger.level; - } else if (isLoweringEdge) { - validTriggerValue = - prevCappedValue != null && - prevCappedValue > state.app.trigger.level && - cappedValue <= state.app.trigger.level; - } + const triggerCategory = state.app.trigger.category; + + const validTriggerValue = + triggerCategory === 'Analog' + ? checkAnalogTriggerValidity( + cappedValue, + prevCappedValue, + state.app.trigger.level, + state.app.trigger.edge + ) + : prevUnsignedBits !== unsignedBits && + checkDigitalTriggerValidity( + unsignedBits, + prevUnsignedBits, + channelTriggerStatuses + ); prevCappedValue = cappedValue; + prevUnsignedBits = unsignedBits; if (!DataManager().isInSync()) { return; diff --git a/src/components/SidePanel/TriggerSettings.tsx b/src/components/SidePanel/AnalogTriggerSettings.tsx similarity index 55% rename from src/components/SidePanel/TriggerSettings.tsx rename to src/components/SidePanel/AnalogTriggerSettings.tsx index d6c55e1d..7a759c1e 100644 --- a/src/components/SidePanel/TriggerSettings.tsx +++ b/src/components/SidePanel/AnalogTriggerSettings.tsx @@ -15,18 +15,11 @@ import { import { appState } from '../../slices/appSlice'; import { getRecordingMode } from '../../slices/chartSlice'; import { - getTriggerBias, getTriggerEdge, - getTriggerRecordingLength, - getTriggerType, getTriggerValue, - setTriggerBias, setTriggerEdge, setTriggerLevel, - setTriggerRecordingLength, - setTriggerType, TriggerEdgeValues, - TriggerTypeValues, } from '../../slices/triggerSlice'; const CurrentUnitValues = ['mA', '\u00B5A'] as const; @@ -50,15 +43,9 @@ const convertToMicroAmps = (unit: CurrentUnit, value: number) => { } }; -const calculateBiasTime = (recordingLength: number, bias: number) => - Number((recordingLength * (bias / 100)).toFixed(2)); - export default () => { const dispatch = useDispatch(); - const recordingLength = useSelector(getTriggerRecordingLength); - const triggerBias = useSelector(getTriggerBias); const triggerValue = useSelector(getTriggerValue); - const triggerType = useSelector(getTriggerType); const triggerEdge = useSelector(getTriggerEdge); const { samplingRunning } = useSelector(appState); const dataLoggerActive = @@ -73,12 +60,6 @@ export default () => { const [internalTriggerValue, setInternalTriggerValue] = useState(triggerValue); - const [internalTriggerLength, setInternalTriggerLength] = - useState(recordingLength); - const [triggerBiasValue, setTriggerBiasValue] = useState(triggerBias); - const [computedBias, setComputedBias] = useState( - calculateBiasTime(internalTriggerLength, triggerBias) - ); useEffect(() => { if (triggerValue > 1000) { @@ -90,62 +71,8 @@ export default () => { } }, [triggerValue]); - useEffect(() => { - setInternalTriggerLength(recordingLength); - }, [recordingLength]); - - useEffect(() => { - setTriggerBiasValue(triggerBias); - }, [triggerBias]); - return ( <> - { - dispatch(setTriggerRecordingLength(value)); - setComputedBias(calculateBiasTime(value, triggerBias)); - }} - unit="ms" - label="Length" - disabled={dataLoggerActive} - showSlider - /> -
- { - dispatch(setTriggerBias(value)); - setComputedBias( - calculateBiasTime(internalTriggerLength, value) - ); - }} - unit="%" - label="Bias" - disabled={dataLoggerActive} - showSlider - /> -
- Computed bias: {computedBias} ms -
-
- { disabled={dataLoggerActive} showSlider /> - dispatch(setTriggerType(TriggerTypeValues[m]))} - selectedItem={triggerType} - disabled={samplingRunning} - /> { diff --git a/src/components/SidePanel/DigitalTriggerSettings.tsx b/src/components/SidePanel/DigitalTriggerSettings.tsx new file mode 100644 index 00000000..882a465c --- /dev/null +++ b/src/components/SidePanel/DigitalTriggerSettings.tsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2015 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause + */ + +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Dropdown, + DropdownItem, +} from '@nordicsemiconductor/pc-nrfconnect-shared'; + +import { + DigitalChannelTriggerStatesEnum, + getDigitalChannelsTriggersStates, + setDigitalChannelsTriggersStates, +} from '../../slices/triggerSlice'; +import { digitalChannelStateTupleOf8 } from '../../utils/persistentStore'; + +const dropdownItems: DropdownItem[] = [ + { + value: DigitalChannelTriggerStatesEnum.Active, + label: 'Active', + }, + { + value: DigitalChannelTriggerStatesEnum.Inactive, + label: 'Inactive', + }, + { + value: DigitalChannelTriggerStatesEnum.DoNotCare, + label: "Don't care", + }, +]; + +export default () => { + const dispatch = useDispatch(); + const digitalChannelTriggerStates = useSelector( + getDigitalChannelsTriggersStates + ); + + const handleDigitalChannelsTriggerStateChange = ( + index: number, + state: DigitalChannelTriggerStatesEnum + ) => { + const newStates = [ + ...digitalChannelTriggerStates, + ] as digitalChannelStateTupleOf8; + newStates[index] = state; + dispatch( + setDigitalChannelsTriggersStates({ + digitalChannelsTriggers: newStates, + }) + ); + }; + + return ( +
+ {digitalChannelTriggerStates.map((state, index) => ( +
+
{`Digital channel ${index}:`}
+ { + handleDigitalChannelsTriggerStateChange( + index, + value.value as DigitalChannelTriggerStatesEnum + ); + }} + items={dropdownItems} + selectedItem={ + dropdownItems.find(item => item.value === state) ?? + dropdownItems[0] + } + /> +
+ ))} +
+ ); +}; diff --git a/src/components/SidePanel/SamplingSettings.tsx b/src/components/SidePanel/SamplingSettings.tsx new file mode 100644 index 00000000..536f8c49 --- /dev/null +++ b/src/components/SidePanel/SamplingSettings.tsx @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2015 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause + */ + +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + NumberInput, + StateSelector, +} from '@nordicsemiconductor/pc-nrfconnect-shared'; + +import { appState } from '../../slices/appSlice'; +import { getRecordingMode } from '../../slices/chartSlice'; +import { + getTriggerBias, + getTriggerRecordingLength, + getTriggerType, + setTriggerBias, + setTriggerRecordingLength, + setTriggerType, + TriggerTypeValues, +} from '../../slices/triggerSlice'; + +const calculateBiasTime = (recordingLength: number, bias: number) => + Number((recordingLength * (bias / 100)).toFixed(2)); + +export default () => { + const dispatch = useDispatch(); + const recordingLength = useSelector(getTriggerRecordingLength); + const triggerBias = useSelector(getTriggerBias); + const triggerType = useSelector(getTriggerType); + const { samplingRunning } = useSelector(appState); + const dataLoggerActive = + useSelector(getRecordingMode) === 'DataLogger' && samplingRunning; + + const [internalTriggerLength, setInternalTriggerLength] = + useState(recordingLength); + const [triggerBiasValue, setTriggerBiasValue] = useState(triggerBias); + const [computedBias, setComputedBias] = useState( + calculateBiasTime(internalTriggerLength, triggerBias) + ); + + useEffect(() => { + setInternalTriggerLength(recordingLength); + }, [recordingLength]); + + useEffect(() => { + setTriggerBiasValue(triggerBias); + }, [triggerBias]); + + return ( + <> + { + dispatch(setTriggerRecordingLength(value)); + setComputedBias(calculateBiasTime(value, triggerBias)); + }} + unit="ms" + label="Length" + disabled={dataLoggerActive} + showSlider + /> + { + dispatch(setTriggerBias(value)); + setComputedBias( + calculateBiasTime(internalTriggerLength, value) + ); + }} + unit="%" + label="Bias" + disabled={dataLoggerActive} + showSlider + /> + + Computed bias: {computedBias} ms + + dispatch(setTriggerType(TriggerTypeValues[m]))} + selectedItem={triggerType} + disabled={samplingRunning} + /> + + ); +}; diff --git a/src/components/SidePanel/StartStop.tsx b/src/components/SidePanel/StartStop.tsx index b441d7d3..9057e0cf 100644 --- a/src/components/SidePanel/StartStop.tsx +++ b/src/components/SidePanel/StartStop.tsx @@ -11,6 +11,7 @@ import { Group, logger, StartStopButton, + StateSelector, telemetry, } from '@nordicsemiconductor/pc-nrfconnect-shared'; import fs from 'fs'; @@ -34,7 +35,12 @@ import { dataLoggerState, getSampleFrequency, } from '../../slices/dataLoggerSlice'; -import { resetTriggerOrigin } from '../../slices/triggerSlice'; +import { + getTriggerCategory, + resetTriggerOrigin, + setTriggerCategory, + TriggerCategoryValues, +} from '../../slices/triggerSlice'; import { convertTimeToSeconds, formatDuration } from '../../utils/duration'; import { calcFileSize, @@ -47,8 +53,10 @@ import { setDoNotAskStartAndClear, } from '../../utils/persistentStore'; import { resetCache } from '../Chart/data/dataAccumulator'; +import AnalogTriggerSettings from './AnalogTriggerSettings'; +import DigitalTriggerSettings from './DigitalTriggerSettings'; import LiveModeSettings from './LiveModeSettings'; -import TriggerSettings from './TriggerSettings'; +import SamplingSettings from './SamplingSettings'; const fmtOpts = { notation: 'fixed' as const, precision: 1 }; @@ -70,6 +78,7 @@ export default () => { const savePending = useSelector(isSavePending); const sessionFolder = useSelector(getSessionRootFolder); const diskFullTrigger = useSelector(getDiskFullTrigger); + const triggerCategory = useSelector(getTriggerCategory); const sampleIndefinitely = durationUnit === 'inf'; @@ -155,9 +164,26 @@ export default () => { return ( <> + {scopePane && ( + + dispatch(setTriggerCategory(TriggerCategoryValues[m])) + } + selectedItem={triggerCategory} + /> + )} {dataLoggerPane && } - {scopePane && } + {scopePane && } + + + {scopePane && triggerCategory === 'Analog' && ( + + )} + {scopePane && triggerCategory === 'Digital' && ( + + )}
({ getAutoExport: () => false, getTriggerType: () => 'Single', getTriggerEdge: () => 'Raising Edge', + getTriggerCategory: () => 'Analog', + getDigitalChannelsTriggers: () => [ + 'Active', + 'Active', + 'Inactive', + 'Inactive', + 'Inactive', + 'Inactive', + 'Inactive', + 'Inactive', + ], })); const getTimestampMock = jest.fn(() => 0); diff --git a/src/components/__tests__/SidePanel.test.tsx b/src/components/__tests__/SidePanel.test.tsx index 4e7084e1..c9b10319 100644 --- a/src/components/__tests__/SidePanel.test.tsx +++ b/src/components/__tests__/SidePanel.test.tsx @@ -37,6 +37,17 @@ jest.mock('../../utils/persistentStore', () => ({ getTriggerEdge: () => 'Raising Edge', getPreferredSessionLocation: () => '/tmp', getDiskFullTrigger: () => 4096, + getTriggerCategory: () => 'Analog', + getDigitalChannelsTriggers: () => [ + 'Active', + 'Active', + 'Inactive', + 'Inactive', + 'Inactive', + 'Inactive', + 'Inactive', + 'Inactive', + ], })); const getTimestampMock = jest.fn(() => 100); diff --git a/src/slices/triggerSlice.ts b/src/slices/triggerSlice.ts index 30e0d9b3..a8ccd664 100644 --- a/src/slices/triggerSlice.ts +++ b/src/slices/triggerSlice.ts @@ -7,34 +7,48 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { + digitalChannelStateTupleOf8, + getDigitalChannelsTriggers as getPersistedDigitalChannelsTriggers, getRecordingLength as getPersistedRecordingLength, getTriggerBias as getPersistedTriggerBias, + getTriggerCategory as getPersistedTriggerCategory, getTriggerEdge as getPersistedTriggerEdge, getTriggerLevel as getPersistedTriggerLevel, getTriggerType as getPersistedTriggerType, + setDigitalChannelsTriggers as persistDigitalChannelsTriggers, setRecordingLength as persistRecordingLength, setTriggerBias as persistTriggerBias, + setTriggerCategory as persistTriggerCategory, setTriggerEdge as persistTriggerEdge, setTriggerLevel as persistTriggerLevel, setTriggerType as persistTriggerType, } from '../utils/persistentStore'; import type { RootState } from '.'; +export const TriggerCategoryValues = ['Digital', 'Analog'] as const; +export type TriggerCategory = (typeof TriggerCategoryValues)[number]; export const TriggerTypeValues = ['Single', 'Continuous'] as const; export type TriggerType = (typeof TriggerTypeValues)[number]; export const TriggerEdgeValues = ['Raising Edge', 'Lowering Edge'] as const; export type TriggerEdge = (typeof TriggerEdgeValues)[number]; +export enum DigitalChannelTriggerStatesEnum { + Active = '1', + Inactive = '0', + DoNotCare = 'X', +} export interface DataLoggerState { level: number; recordingLength: number; bias: number; active: boolean; + category: TriggerCategory; type: TriggerType; edge: TriggerEdge; progressMessage?: string; progress?: number; triggerOrigin?: number; + digitalChannelsTriggersStates: digitalChannelStateTupleOf8; } const initialState = (): DataLoggerState => ({ @@ -42,8 +56,12 @@ const initialState = (): DataLoggerState => ({ recordingLength: getPersistedRecordingLength(1000), bias: getPersistedTriggerBias(0), active: false, + category: getPersistedTriggerCategory('Analog'), type: getPersistedTriggerType('Single'), edge: getPersistedTriggerEdge('Raising Edge'), + digitalChannelsTriggersStates: getPersistedDigitalChannelsTriggers( + [] as unknown as digitalChannelStateTupleOf8 + ), }); const triggerSlice = createSlice({ @@ -66,6 +84,10 @@ const triggerSlice = createSlice({ setTriggerActive: (state, action: PayloadAction) => { state.active = action.payload; }, + setTriggerCategory: (state, action: PayloadAction) => { + state.category = action.payload; + persistTriggerCategory(action.payload); + }, setTriggerType: (state, action: PayloadAction) => { state.type = action.payload; persistTriggerType(action.payload); @@ -74,6 +96,18 @@ const triggerSlice = createSlice({ state.edge = action.payload; persistTriggerEdge(action.payload); }, + setDigitalChannelsTriggersStates: ( + state, + action: PayloadAction<{ + digitalChannelsTriggers: digitalChannelStateTupleOf8; + }> + ) => { + state.digitalChannelsTriggersStates = + action.payload.digitalChannelsTriggers; + persistDigitalChannelsTriggers( + action.payload.digitalChannelsTriggers + ); + }, setProgress: ( state, action: PayloadAction<{ @@ -101,8 +135,12 @@ export const getTriggerRecordingLength = (state: RootState) => state.app.trigger.recordingLength; export const getTriggerBias = (state: RootState) => state.app.trigger.bias; export const getTriggerActive = (state: RootState) => state.app.trigger.active; +export const getTriggerCategory = (state: RootState) => + state.app.trigger.category; export const getTriggerType = (state: RootState) => state.app.trigger.type; export const getTriggerEdge = (state: RootState) => state.app.trigger.edge; +export const getDigitalChannelsTriggersStates = (state: RootState) => + state.app.trigger.digitalChannelsTriggersStates; export const getProgress = (state: RootState) => ({ progressMessage: state.app.trigger.progressMessage, progress: state.app.trigger.progress, @@ -115,8 +153,10 @@ export const { setTriggerRecordingLength, setTriggerBias, setTriggerActive, + setTriggerCategory, setTriggerType, setTriggerEdge, + setDigitalChannelsTriggersStates, setProgress, clearProgress, setTriggerOrigin, diff --git a/src/utils/persistentStore.ts b/src/utils/persistentStore.ts index 5172476f..a049be2f 100644 --- a/src/utils/persistentStore.ts +++ b/src/utils/persistentStore.ts @@ -9,7 +9,12 @@ import { getPersistentStore, } from '@nordicsemiconductor/pc-nrfconnect-shared'; -import type { TriggerEdge, TriggerType } from '../slices/triggerSlice'; +import { + type DigitalChannelTriggerStatesEnum, + type TriggerCategory, + type TriggerEdge, + type TriggerType, +} from '../slices/triggerSlice'; const LAST_SAVE_DIR = 'lastSaveDir'; const SPIKE_FILTER_SAMPLES = 'spikeFilter.samples'; @@ -17,6 +22,7 @@ const SPIKE_FILTER_ALPHA = 'spikeFilter.alpha'; const SPIKE_FILTER_ALPHA5 = 'spikeFilter.alpha5'; const DIGITAL_CHANNELS_VISIBLE = 'digitalChannelsVisible'; const DIGITAL_CHANNELS = 'digitalChannels'; +const DIGITAL_CHANNELS_TRIGGERS = 'digitalChannelsTriggers'; const TIMESTAMPS_VISIBLE = 'timestampsVisible'; const VOLTAGE_REGULATOR_MAX_CAP_PPK2 = 'voltageRegulatorMaxCap'; @@ -48,6 +54,17 @@ export type booleanTupleOf8 = [ boolean ]; +export type digitalChannelStateTupleOf8 = [ + DigitalChannelTriggerStatesEnum, + DigitalChannelTriggerStatesEnum, + DigitalChannelTriggerStatesEnum, + DigitalChannelTriggerStatesEnum, + DigitalChannelTriggerStatesEnum, + DigitalChannelTriggerStatesEnum, + DigitalChannelTriggerStatesEnum, + DigitalChannelTriggerStatesEnum +]; + export type TimeUnit = 's' | 'm' | 'h' | 'd' | 'inf'; interface StoreSchema { @@ -60,6 +77,8 @@ interface StoreSchema { [DIGITAL_CHANNELS]: booleanTupleOf8; [TIMESTAMPS_VISIBLE]: boolean; + [DIGITAL_CHANNELS_TRIGGERS]: digitalChannelStateTupleOf8; + [VOLTAGE_REGULATOR_MAX_CAP_PPK2]: number; [maxSampleFrequency: SAMPLE_FREQUENCY]: number; @@ -141,6 +160,11 @@ export const getTriggerBias = (defaultValue: number) => export const setTriggerBias = (value: number) => { store.set(`trigger-bias-ms`, value); }; +export const getTriggerCategory = (defaultValue: TriggerCategory) => + store.get(`trigger-category`, defaultValue); +export const setTriggerCategory = (value: TriggerCategory) => { + store.set(`trigger-category`, value); +}; export const getTriggerType = (defaultValue: TriggerType) => store.get(`trigger-mode-type`, defaultValue); export const setTriggerType = (value: TriggerType) => { @@ -151,6 +175,12 @@ export const getTriggerEdge = (defaultValue: TriggerEdge) => export const setTriggerEdge = (value: TriggerEdge) => { store.set(`trigger-edge`, value); }; +export const getDigitalChannelsTriggers = ( + defaultValue: digitalChannelStateTupleOf8 +) => store.get(DIGITAL_CHANNELS_TRIGGERS, defaultValue); +export const setDigitalChannelsTriggers = ( + triggers: digitalChannelStateTupleOf8 +) => store.set(DIGITAL_CHANNELS_TRIGGERS, triggers); export const getDurationUnit = ( maxSampleFreq: number,