Skip to content

Commit

Permalink
Merge pull request #149 from concord-consortium/187809451-add-sample-…
Browse files Browse the repository at this point in the history
…rate-picker

feat: Added sample rate picker [PT-187809451]
  • Loading branch information
dougmartin authored Jun 25, 2024
2 parents c1cda90 + 2e9dabf commit 6c49aaa
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 43 deletions.
27 changes: 27 additions & 0 deletions src/mobile-app/components/sensor.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@
border-right: 2px solid #bfe9c2;
margin: 0 5px;
}

.tsvRight {
.tsvInfo {
display: flex;
flex-direction: column;
gap: 6px;

.tsvInfoRow {
display: flex;
align-items: center;
text-align: right;
justify-content: flex-end;
gap: 10px;

div:last-child {
min-width: 100px;
text-align: left;
}
}

select {
padding: 8px;
border-radius: 5px;
background-color: #fff;
}
}
}
}

.sensorSelector {
Expand Down
80 changes: 65 additions & 15 deletions src/mobile-app/components/sensor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from "react";
import React, { useState, useRef, useMemo, useEffect } from "react";
import { SensorValue } from "./sensor-value";
import { Sensor, IConnectDevice, SelectDeviceFn, ITimeSeriesCapabilities, MaxNumberOfTimeSeriesValues, ISensorValues } from "../../sensors/sensor";
import { useSensor } from "../hooks/use-sensor";
Expand All @@ -17,7 +17,7 @@ interface ISensorSelectorProps {
cancel: () => void;
}

const maxTimeSeriesValues = 5;
const maxTime = 3;

export const SensorSelectorComponent: React.FC<ISensorSelectorProps> = ({devices, selectDevice, cancel}) => {
const sortedDevices = devices.sort((a, b) => a.id.localeCompare(b.id));
Expand Down Expand Up @@ -51,6 +51,7 @@ interface ISensorComponentProps {
setManualEntryMode?: (flag: boolean) => void;
isTimeSeries: boolean;
timeSeriesCapabilities?: ITimeSeriesCapabilities;
setTimeSeriesMeasurementPeriod?: (measurementPeriod: number) => void;
}

const iconClass = {
Expand All @@ -65,14 +66,51 @@ const iconClassHi = {
error: css.errorIcon
};

export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manualEntryMode, setManualEntryMode, isTimeSeries, timeSeriesCapabilities}) => {
export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manualEntryMode, setManualEntryMode, isTimeSeries, timeSeriesCapabilities, setTimeSeriesMeasurementPeriod}) => {
const {connected, connecting, deviceName, values, error} = useSensor(sensor);

const [devicesFound, setDevicesFound] = useState<IConnectDevice[]>([]);
const [showDeviceSelect, setShowDeviceSelect] = useState(false);
const selectDevice = useRef<SelectDeviceFn|undefined>();
const cancelSelectDevice = useRef<CancelDeviceFn|undefined>();

// generate a sliding window of time series values
const latestValuesRef = useRef<IDataTableTimeData[]>([]);
useEffect(() => {
if (!sensor.connected || !isTimeSeries || !timeSeriesCapabilities) {
latestValuesRef.current = [];
return;
}

const valueKey = timeSeriesCapabilities.valueKey as keyof ISensorValues;
const timeDelta = sensor.pollInterval / 1000;
const hasValues = latestValuesRef.current.length > 0;
let latestTime = hasValues ? latestValuesRef.current[latestValuesRef.current.length - 1].time : 0;
let nextTime = hasValues ? latestTime + timeDelta : 0;
if (latestTime >= maxTime) {
nextTime = maxTime;
latestTime = maxTime;
latestValuesRef.current = latestValuesRef.current.slice(1).map(({value}, index) => {
return {value, time: index * timeDelta};
});
}
latestValuesRef.current.push({value: values[valueKey] ?? 0, time: nextTime});
latestValuesRef.current[0].capabilities = timeSeriesCapabilities;
}, [isTimeSeries, sensor, timeSeriesCapabilities, values]);

const timeSeriesPeriods = useMemo(() => {
if (!timeSeriesCapabilities) {
return [];
}

const {measurementPeriod, minMeasurementPeriod, defaultMeasurementPeriod} = timeSeriesCapabilities;

const result: number[] = [10, 20, 50, 200, 500, 1000, 2000, 10000, measurementPeriod, defaultMeasurementPeriod, minMeasurementPeriod]
.filter(n => n >= minMeasurementPeriod)
.filter((item, pos, self) => self.indexOf(item) === pos);
result.sort((a, b) => b - a);
return result;
}, [timeSeriesCapabilities]);

const clearSelectDevice = () => {
selectDevice.current = undefined;
Expand All @@ -90,6 +128,11 @@ export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manual
clearSelectDevice();
};

const handleMeasurementPeriodChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newPeriod = parseInt(e.target.value, 10);
setTimeSeriesMeasurementPeriod?.(newPeriod);
};

const connect = () => sensor.connect({
onDevicesFound: ({devices, select, cancel}) => {
setDevicesFound(devices);
Expand Down Expand Up @@ -160,17 +203,12 @@ export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manual
return <div className={css.timeSeriesValue} />;
}

const {measurementPeriod, measurement, units, minValue, maxValue} = timeSeriesCapabilities;
const {measurementPeriod, measurement, units} = timeSeriesCapabilities;
const valueKey = timeSeriesCapabilities.valueKey as keyof ISensorValues;
const sampleRate = measurementPeriod / 1000;
const maxSamples = sampleRate * MaxNumberOfTimeSeriesValues;
const maxSampleTime = sampleRate * MaxNumberOfTimeSeriesValues;
const value = values[valueKey];
const displayValue = value !== undefined ? value.toFixed(1) : "--";
while (latestValuesRef.current.length > maxTimeSeriesValues) {
latestValuesRef.current.shift();
}
latestValuesRef.current.push({value: value ?? 0, time: 0});
latestValuesRef.current[0].capabilities = timeSeriesCapabilities;

return (
<div className={css.timeSeriesValue}>
Expand All @@ -180,8 +218,8 @@ export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manual
width={25}
height={50}
values={latestValuesRef.current}
minNumTimeSeriesValues={maxTimeSeriesValues}
maxNumTimeSeriesValues={maxTimeSeriesValues}
minTime={0}
maxTime={maxTime}
showAxes={true}
redrawSignal={Date.now()}
/>
Expand All @@ -195,9 +233,21 @@ export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manual
</div>
</div>
<div className={css.tsvSeparator} />
<div>
<div>Samples: {sampleRate}/sec</div>
<div>Max Time: {maxSamples} secs</div>
<div className={css.tsvRight}>
<div className={css.tsvInfo}>
<div className={css.tsvInfoRow}>
<div>Samples:</div>
<div>
<select className={css.tsvSampleRate} value={measurementPeriod} onChange={handleMeasurementPeriodChange}>
{timeSeriesPeriods.map(p => <option key={p} value={p}>{`${(1000 / p)}/sec`}</option>)}
</select>
</div>
</div>
<div className={css.tsvInfoRow}>
<div>Max Time:</div>
<div>{maxSampleTime} secs</div>
</div>
</div>
</div>
</div>
);
Expand Down
6 changes: 5 additions & 1 deletion src/sensors/devices/gdx-sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ISensorCapabilities, ISensorValues, ITimeSeriesCapabilities } from "../
import { IDataTableTimeData } from "../../shared/components/data-table-field";

const goDirectServiceUUID = "d91714ef-28b9-4f91-ba16-f0d9a604f112";
const defaultMeasurementPeriod = 100;

// NOTE: to add a new sensor using the existing global capabilities add the prefix to the array
// and update the mapping of the sensor name to the capability below. More work will be
Expand Down Expand Up @@ -96,13 +95,18 @@ export class GDXSensorDevice extends Device {
return;
}

let defaultMeasurementPeriod = 50;
const firstSensor = gdxDevice?.sensors[0];
if (firstSensor) {
const measurementInfo = firstSensor.specs?.measurementInfo;
const measurement: string = firstSensor.name;
const valueKey = measurement.toLowerCase();
const minMeasurementPeriod = gdxDevice.minMeasurementPeriod;
defaultMeasurementPeriod = Math.max(minMeasurementPeriod, defaultMeasurementPeriod);
this._timeSeriesCapabilities = {
measurementPeriod: defaultMeasurementPeriod,
minMeasurementPeriod,
defaultMeasurementPeriod,
measurement,
valueKey,
units: firstSensor.unit,
Expand Down
5 changes: 4 additions & 1 deletion src/sensors/mock-sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,11 @@ export class MockSensor extends Sensor {
}

public get timeSeriesCapabilities(): ITimeSeriesCapabilities {
const defaultMeasurementPeriod = 50;
return {
measurementPeriod: 100,
measurementPeriod: defaultMeasurementPeriod,
minMeasurementPeriod: 10,
defaultMeasurementPeriod,
measurement: "Fake Value",
valueKey: "timeSeries",
units: "N/A",
Expand Down
12 changes: 9 additions & 3 deletions src/sensors/sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export interface IConnectOptions {

export interface ITimeSeriesCapabilities {
measurementPeriod: number;
minMeasurementPeriod: number;
defaultMeasurementPeriod: number;
measurement: string;
valueKey: string;
units: string;
Expand All @@ -96,7 +98,7 @@ export class Sensor extends EventEmitter<SensorEvent> {
private _experimentFilters: BluetoothRequestDeviceFilter[];
private _capabilities: ISensorCapabilities;
private pollTimeout: number;
private pollInterval: number;
private _pollInterval: number;
private error: any;

constructor(options: ISensorOptions) {
Expand All @@ -105,7 +107,7 @@ export class Sensor extends EventEmitter<SensorEvent> {
this._capabilities = options.capabilities;
this._connected = false;
this._values = {};
this.pollInterval = options.pollInterval || 1000;
this._pollInterval = options.pollInterval || 1000;
this.error = undefined;
}

Expand All @@ -121,6 +123,10 @@ export class Sensor extends EventEmitter<SensorEvent> {
return undefined; // set in device
}

public get pollInterval() {
return this._pollInterval;
}

public get connected() {
return this._connected;
}
Expand Down Expand Up @@ -193,7 +199,7 @@ export class Sensor extends EventEmitter<SensorEvent> {
const createTimeout = () => {
this.pollTimeout = window.setTimeout(() => {
this.sendData(options).then(() => this.poll({firstPoll: false}));
}, this.pollInterval);
}, this._pollInterval);
};

if (options.firstPoll) {
Expand Down
38 changes: 24 additions & 14 deletions src/shared/components/data-table-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { useSensor } from "../../mobile-app/hooks/use-sensor";
import { tableKeyboardNav } from "../utils/table-keyboard-nav";
import { confirm, alert } from "../utils/dialogs";
import { handleSpecialValue, isFunctionSymbol } from "../utils/handle-special-value";
import css from "./data-table-field.module.scss";
import DataTableSparkGraph from "./data-table-sparkgraph";

import css from "./data-table-field.module.scss";

const defPrecision = 2;

// Schema that is accepted by this component.
Expand Down Expand Up @@ -179,9 +180,9 @@ export const DataTableField: React.FC<FieldProps> = props => {
const waitForSensorIntervalRef = useRef(0);
const stopTimeSeriesFnRef = useRef<(() => void)|undefined>(undefined);
const timeSeriesRecordingRowRef = useRef<number|undefined>(undefined);
const maxNumTimeSeriesValues = useMemo(() => {
const maxTime = useMemo(() => {
const result = isTimeSeries ? formData.reduce<number>((acc, {timeSeries}) => {
return Math.max(acc, Array.isArray(timeSeries) ? timeSeries.length : 0);
return Math.max(acc, Array.isArray(timeSeries) ? timeSeries[timeSeries.length - 1].time : 0);
}, 0) : 0;
return result;
}, [isTimeSeries, formData]);
Expand All @@ -200,7 +201,7 @@ export const DataTableField: React.FC<FieldProps> = props => {
waitForSensorIntervalRef.current = setInterval(() => {
const result = sensor.timeSeriesCapabilities;
if (result) {
setTimeSeriesCapabilities(result);
setTimeSeriesCapabilities({...result});
clearInterval(waitForSensorIntervalRef.current);
}
});
Expand All @@ -209,6 +210,10 @@ export const DataTableField: React.FC<FieldProps> = props => {
}
}, [sensor, sensorOutput.connected, isTimeSeries, setTimeSeriesCapabilities]);

const setTimeSeriesMeasurementPeriod = (newPeriod: number) => {
setTimeSeriesCapabilities(prev => prev ? {...prev, measurementPeriod: newPeriod} : prev);
};

const sensorCanRecord = useMemo(() => {
return sensorOutput.connected && Object.values(sensorOutput.values).length > 0;
}, [sensorOutput]);
Expand Down Expand Up @@ -345,11 +350,11 @@ export const DataTableField: React.FC<FieldProps> = props => {
};

const recordTimeSeries = () => {
if (!sensor) {
if (!sensor || !timeSeriesCapabilities) {
return;
}

stopTimeSeriesFnRef.current = sensor.collectTimeSeries(100, (values) => {
stopTimeSeriesFnRef.current = sensor.collectTimeSeries(timeSeriesCapabilities.measurementPeriod, (values) => {
const newData = formData.slice();
newData[rowIdx] = {timeSeries: values};
if (values.length <= MaxNumberOfTimeSeriesValues) {
Expand Down Expand Up @@ -487,12 +492,8 @@ export const DataTableField: React.FC<FieldProps> = props => {

let contents;
if (name === "timeSeries") {
let graphTitle = "";
const values= value || [];
if (timeSeriesCapabilities) {
const duration = Math.round((timeSeriesCapabilities.measurementPeriod / 1000) * values.length);
graphTitle = `${duration} sec`;
}
const values: IDataTableTimeData[] = value || [];
const graphTitle = values.length > 0 ? `${Math.round(values[values.length - 1].time)} sec` : "";

contents =
<div className={css.sparkgraphContainer}>
Expand All @@ -501,7 +502,7 @@ export const DataTableField: React.FC<FieldProps> = props => {
width={200}
height={30}
values={value || []}
maxNumTimeSeriesValues={maxNumTimeSeriesValues}
maxTime={maxTime}
/>
</div>;
} else if (readOnly) {
Expand Down Expand Up @@ -541,7 +542,16 @@ export const DataTableField: React.FC<FieldProps> = props => {
<div className={css.dataTable}>
<div className={css.topBar}>
<div className={css.topBarLeft}>
{showSensor && sensor ? <SensorComponent sensor={sensor} manualEntryMode={manualEntryMode} setManualEntryMode={showShowSensorButton ? undefined : setManualEntryMode} isTimeSeries={isTimeSeries} timeSeriesCapabilities={timeSeriesCapabilities} /> : undefined}
{showSensor && sensor
? <SensorComponent
sensor={sensor}
manualEntryMode={manualEntryMode}
setManualEntryMode={showShowSensorButton ? undefined : setManualEntryMode}
isTimeSeries={isTimeSeries}
timeSeriesCapabilities={timeSeriesCapabilities}
setTimeSeriesMeasurementPeriod={setTimeSeriesMeasurementPeriod}
/>
: undefined}
{title ? <div className={css.title}>{title}</div> : undefined}
</div>
<div className={css.topBarRight}>
Expand Down
Loading

0 comments on commit 6c49aaa

Please sign in to comment.