Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added sample rate picker [PT-187809451] #149

Merged
merged 1 commit into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading