import React from "react"; import { observable, computed, runInAction, action, toJS, when, reaction, makeObservable, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import { objectEqual } from "eez-studio-shared/util"; import { beginTransaction, commitTransaction, IStore } from "eez-studio-shared/store"; import { UNITS, TIME_UNIT } from "eez-studio-shared/units"; import { scheduleTask, Priority } from "eez-studio-shared/scheduler"; import { Point } from "eez-studio-shared/geometry"; import type * as I10nModule from "eez-studio-shared/i10n"; import { Dialog, showDialog } from "eez-studio-ui/dialog"; import { PropertyList } from "eez-studio-ui/properties"; import { ChartController, ChartMode, IAxisModel, LineController, ChartsController, IChartsController, MeasurementsModel, getNearestValuePoint, IAxisController, IMeasurementsModel } from "eez-studio-ui/chart/chart"; import { RulersModel, IRulersModel } from "eez-studio-ui/chart/rulers"; import { WaveformLineView } from "eez-studio-ui/chart/WaveformLineView"; import { WaveformFormat } from "eez-studio-ui/chart/WaveformFormat"; import { initValuesAccesor } from "eez-studio-ui/chart/value-accesor"; import * as notification from "eez-studio-ui/notification"; import { logUpdate, IActivityLogEntry } from "instrument/window/history/activity-log"; import { checkMime } from "instrument/connection/file-type"; import { ChartPreview } from "instrument/window/chart-preview"; import { FileHistoryItem } from "instrument/window/history/items/file"; import { WaveformTimeAxisModel } from "instrument/window/waveform/time-axis"; import { IToolbarOptions, WaveformToolbar } from "instrument/window/waveform/toolbar"; import type { ChartsDisplayOption } from "instrument/window/lists/common-tools"; import { ViewOptions } from "instrument/window/waveform/ViewOptions"; import { WaveformAxisModel } from "instrument/window/waveform/WaveformAxisModel"; import { WaveformDefinitionProperties } from "instrument/window/waveform/WaveformDefinitionProperties"; import type { IAppStore } from "instrument/window/history/history"; import { capitalize } from "eez-studio-shared/string"; //////////////////////////////////////////////////////////////////////////////// export interface IWaveformDefinition { samplingRate: number; format: WaveformFormat; unitName: keyof typeof UNITS; color?: string; colorInverse?: string; label?: string; offset: number; scale: number; cachedMinValue: number; cachedMaxValue: number; } export interface IWaveformHistoryItemMessage { waveformDefinition: IWaveformDefinition; viewOptions: ViewOptions; rulers: RulersModel; measurements: RulersModel; horizontalScale?: number; verticalScale?: number; } //////////////////////////////////////////////////////////////////////////////// export function isWaveform(activityLogEntry: IActivityLogEntry) { return ( (activityLogEntry as any).waveformDefinition || checkMime(activityLogEntry.message, [ "application/eez-binary-list", "application/eez-raw", "text/csv" ]) ); } //////////////////////////////////////////////////////////////////////////////// export class WaveformChartsController extends ChartsController { constructor( public waveform: Waveform, mode: ChartMode, xAxisModel: IAxisModel ) { super(mode, xAxisModel, waveform.viewOptions); } get chartViewOptionsProps() { return { showRenderAlgorithm: true, showShowSampledDataOption: false }; } get supportRulers() { return true; } } //////////////////////////////////////////////////////////////////////////////// export class Waveform extends FileHistoryItem { dispose1: IReactionDisposer | undefined; dispose2: IReactionDisposer | undefined; dispose3: IReactionDisposer | undefined; dispose4: IReactionDisposer | undefined; dispose5: IReactionDisposer | undefined; canBePartOfMultiChart = true; constructor( store: IStore, activityLogEntry: IActivityLogEntry | FileHistoryItem, private options?: { toolbar?: IToolbarOptions } ) { super(store, activityLogEntry); makeObservable(this, { initWaveformDefinition: action.bound, values: computed, waveformHistoryItemMessage: computed, waveformDefinition: observable.shallow, length: observable, xAxisUnit: computed, samplingRate: computed, yAxisDefaultSubdivisionOffsetAndScale: computed }); const message = JSON.parse(this.message); this.viewOptions = new ViewOptions(message.viewOptions); this.xAxisModel.dynamic.zoomMode = this.xAxisModel.fixed.zoomMode = this.viewOptions.axesLines.defaultZoomMode || "all"; this.rulers = new RulersModel(message.rulers); this.rulers.initYRulers(1); this.measurements = new MeasurementsModel(message.measurements); when( () => this.transferSucceeded, () => { scheduleTask( `Load waveform ${this.id}`, Priority.Lowest, async () => this.initWaveformDefinition() ); } ); // save waveformDefinition when changed this.dispose1 = reaction( () => toJS(this.waveformDefinition), waveformDefinition => { const message = JSON.parse(this.message); if ( !objectEqual(message.waveformDefinition, waveformDefinition) ) { const messageStr = JSON.stringify( Object.assign(message, { waveformDefinition }) ); runInAction(() => (this.message = messageStr)); logUpdate( this.store, { id: this.id, oid: this.oid, message: messageStr }, { undoable: false } ); } } ); // save viewOptions when changed this.dispose2 = reaction( () => toJS(this.viewOptions), viewOptions => { const message = JSON.parse(this.message); if (!objectEqual(message.viewOptions, viewOptions)) { const messageStr = JSON.stringify( Object.assign(message, { viewOptions }) ); runInAction(() => (this.message = messageStr)); logUpdate( this.store, { id: this.id, oid: this.oid, message: messageStr }, { undoable: false } ); } } ); // save rulers when changed this.dispose3 = reaction( () => toJS(this.rulers), rulers => { if (rulers.pauseDbUpdate) { return; } delete rulers.pauseDbUpdate; const message = JSON.parse(this.message); if (!objectEqual(message.rulers, rulers)) { const messageStr = JSON.stringify( Object.assign(message, { rulers }) ); runInAction(() => (this.message = messageStr)); logUpdate( this.store, { id: this.id, oid: this.oid, message: messageStr }, { undoable: false } ); } } ); // save measurements when changed this.dispose4 = reaction( () => toJS(this.measurements), measurements => { const message = JSON.parse(this.message); if (!objectEqual(message.measurements, measurements)) { const messageStr = JSON.stringify( Object.assign(message, { measurements }) ); runInAction(() => (this.message = messageStr)); logUpdate( this.store, { id: this.id, oid: this.oid, message: messageStr }, { undoable: false } ); } } ); // this.dispose5 = reaction( () => JSON.parse(this.message), message => { const waveformDefinition = toJS(this.waveformDefinition); if ( !objectEqual(message.waveformDefinition, waveformDefinition) ) { this.initWaveformDefinition(); } } ); } initValuesAccesor() { initValuesAccesor(this); } findRange() { let minValue = Number.POSITIVE_INFINITY; let maxValue = Number.NEGATIVE_INFINITY; if (this.length > 0) { minValue = this.waveformData(0); maxValue = this.waveformData(0); for (let i = 1; i < this.length; i++) { const value = this.waveformData(i); if (value < minValue) { minValue = value; } else if (value > maxValue) { maxValue = value; } } } else { minValue = 0; maxValue = 0; } this.waveformDefinition.cachedMinValue = minValue; this.waveformDefinition.cachedMaxValue = maxValue; } guessWaveformFormat() { let format: WaveformFormat = WaveformFormat.UNKNOWN; if (this.fileTypeAsDisplayString === "text/csv") { format = WaveformFormat.CSV_STRING; } else { format = WaveformFormat.RIGOL_BYTE; } return format; } getDefaultWaveformDefinition(): IWaveformDefinition { return { samplingRate: 1, format: this.guessWaveformFormat(), unitName: "unknown", offset: 0, scale: 1, cachedMinValue: 0, cachedMaxValue: 0 }; } migrateWaveformDefinition() { let migrated = false; if (this.waveformDefinition.samplingRate === undefined) { this.waveformDefinition.samplingRate = 1000000; migrated = true; } if (this.waveformDefinition.offset === undefined) { this.waveformDefinition.offset = 0; migrated = true; } if (this.waveformDefinition.scale === undefined) { this.waveformDefinition.scale = 1; migrated = true; } if ( this.waveformDefinition.cachedMinValue == null || this.waveformDefinition.cachedMaxValue == null ) { migrated = true; } return migrated; } initWaveformDefinition() { let migrated = false; if (this.waveformHistoryItemMessage.waveformDefinition) { const oldFormat = this.waveformDefinition && this.waveformDefinition.format; const oldOffset = this.waveformDefinition && this.waveformDefinition.offset; const oldScale = this.waveformDefinition && this.waveformDefinition.scale; this.waveformDefinition = this.waveformHistoryItemMessage.waveformDefinition; migrated = this.migrateWaveformDefinition(); if (!migrated) { if ( oldFormat !== this.waveformDefinition.format || oldOffset !== this.waveformDefinition.offset || oldScale !== this.waveformDefinition.scale ) { // recalculate range migrated = true; } } } else { this.waveformDefinition = this.getDefaultWaveformDefinition(); migrated = true; } this.initValuesAccesor(); if (migrated) { this.findRange(); } } get values(): any { if (typeof this.data === "string") { return new Uint8Array(Buffer.from(this.data, "binary").buffer); } return this.data; } get waveformHistoryItemMessage(): IWaveformHistoryItemMessage { return JSON.parse(this.message); } waveformDefinition = this.getDefaultWaveformDefinition(); length: number = 0; get format() { return this.waveformDefinition.format; } get offset() { return this.waveformDefinition.offset; } set offset(value: number) { this.waveformDefinition.offset = value; } get scale() { return this.waveformDefinition.scale; } set scale(value: number) { this.waveformDefinition.scale = value; } get xAxisUnit() { return TIME_UNIT; } get samplingRate() { return this.waveformDefinition.samplingRate; } viewOptions: ViewOptions; rulers: IRulersModel; measurements: IMeasurementsModel; xAxisModel = new WaveformTimeAxisModel(this); chartsController: IChartsController; createChartsController( appStore: IAppStore, displayOption: ChartsDisplayOption, mode: ChartMode ): IChartsController { if ( this.chartsController && this.chartsController.mode === mode && this.chartsController.xAxisController.axisModel === this.xAxisModel && this.chartsController.chartControllers[0].yAxisController .axisModel === this.yAxisModel ) { return this.chartsController; } if (this.chartsController) { this.chartsController.destroy(); } const chartsController = new WaveformChartsController( this, mode, this.xAxisModel ); this.chartsController = chartsController; this.xAxisModel.chartsController = chartsController; chartsController.chartControllers = [ this.createChartController( chartsController, "unknown", this.yAxisModel ) ]; if (!chartsController.isMultiWaveformChartsController) { chartsController.createRulersController(this.rulers); chartsController.createMeasurementsController(this.measurements); } return chartsController; } createChartController( chartsController: IChartsController, id: string, axisModel: IAxisModel ) { const chartController = new ChartController(chartsController, id); chartController.createYAxisController(axisModel); chartController.lineControllers.push( new WaveformLineController( "waveform-" + chartController.yAxisController.position, this, chartController.yAxisController ) ); return chartController; } yAxisModel = new WaveformAxisModel(this, undefined); value(index: number) { return 0; } waveformData(index: number) { return 0; } waveformDataToValue(waveformDataValue: number) { return this.offset + waveformDataValue * this.scale; } get minValue() { if ( this.waveformDefinition.format === WaveformFormat.RIGOL_BYTE || this.waveformDefinition.format === WaveformFormat.RIGOL_WORD ) { return this.waveformDataToValue( this.waveformDefinition.cachedMinValue ); } else { return this.waveformDefinition.cachedMinValue; } } get maxValue() { if ( this.waveformDefinition.format === WaveformFormat.RIGOL_BYTE || this.waveformDefinition.format === WaveformFormat.RIGOL_WORD ) { return this.waveformDataToValue( this.waveformDefinition.cachedMaxValue ); } else { return this.waveformDefinition.cachedMaxValue; } } renderToolbar(chartsController: IChartsController): JSX.Element { return ( ); } openConfigurationDialog() { showDialog(); } get xAxisDefaultSubdivisionOffset(): number | undefined { return this.waveformHistoryItemMessage.horizontalScale !== undefined ? 0 : undefined; } get xAxisDefaultSubdivisionScale() { return this.waveformHistoryItemMessage.horizontalScale; } get yAxisDefaultSubdivisionOffsetAndScale() { if (this.waveformHistoryItemMessage.verticalScale) { const verticalScale = this.waveformHistoryItemMessage.verticalScale; const min = Math.floor(this.yAxisModel.minValue / verticalScale) * verticalScale; const max = Math.ceil(this.yAxisModel.maxValue / verticalScale) * verticalScale; const subdivision = this.waveformHistoryItemMessage.viewOptions.axesLines .majorSubdivision.vertical; return { offset: (min + max) / 2 - (verticalScale * subdivision) / 2, scale: verticalScale }; } return { offset: undefined, scale: undefined }; } get yAxisDefaultSubdivisionOffset(): number | undefined { return this.yAxisDefaultSubdivisionOffsetAndScale.offset; } get yAxisDefaultSubdivisionScale() { return this.yAxisDefaultSubdivisionOffsetAndScale.scale; } getPreviewElement(appStore: IAppStore) { return ; } get valueUnit() { return this.waveformDefinition.unitName; } isZoomable = true; convertToCsv = () => { return convertToCsv(this); }; override dispose() { super.dispose(); if (this.dispose1) { this.dispose1(); } if (this.dispose2) { this.dispose2(); } if (this.dispose3) { this.dispose3(); } if (this.dispose4) { this.dispose4(); } if (this.dispose5) { this.dispose5(); } } } //////////////////////////////////////////////////////////////////////////////// class WaveformLineController extends LineController { constructor( public id: string, public waveform: Waveform, yAxisController: IAxisController ) { super(id, yAxisController); makeObservable(this, { yMin: computed, yMax: computed }); } get yMin(): number { return this.yAxisController.axisModel.minValue; } get yMax(): number { return this.yAxisController.axisModel.maxValue; } getWaveformModel() { return this.waveform; } getNearestValuePoint(point: Point): Point { return getNearestValuePoint( point, this.xAxisController, this.yAxisController, this.waveform ); } render(): JSX.Element { return ; } } //////////////////////////////////////////////////////////////////////////////// const WaveformConfigurationDialog = observer( class WaveformConfigurationDialog extends React.Component<{ waveform: Waveform; }> { waveformProperties: WaveformDefinitionProperties = new WaveformDefinitionProperties( this.props.waveform.waveformDefinition ); handleSubmit = async () => { const newWaveformDefinition = await this.waveformProperties.checkValidity(); if (!newWaveformDefinition) { return false; } if ( !objectEqual( this.props.waveform.waveformDefinition, newWaveformDefinition ) ) { const message = JSON.stringify( Object.assign({}, this.props.waveform.fileState, { waveformDefinition: newWaveformDefinition }) ); beginTransaction("Edit waveform configuration"); logUpdate( this.props.waveform.store, { id: this.props.waveform.id, oid: this.props.waveform.oid, message }, { undoable: true } ); commitTransaction(); } return true; }; render() { return ( {this.waveformProperties.render()} ); } } ); export async function convertToCsv(waveform: Waveform) { const { getLocale } = require("eez-studio-shared/i10n") as typeof I10nModule; const locale = getLocale(); // determine CSV separator depending of locale usage of "," let separator; if ((0.1).toLocaleString(locale).indexOf(",") != -1) { separator = ";"; } else { separator = ","; } const numberFormat = new Intl.NumberFormat(locale, { useGrouping: false, maximumFractionDigits: 19 }); let csv = ""; let progressToastId = notification.info("Exporting to CSV ...", { autoClose: false, closeButton: false, closeOnClick: false, hideProgressBar: false, progressStyle: { transition: "none" } }); await new Promise(resolve => setTimeout(resolve, 0)); csv += `Time,${ waveform.waveformDefinition.label || capitalize(waveform.waveformDefinition.unitName) }\n`; csv += `(ms),(${capitalize( UNITS[waveform.waveformDefinition.unitName].unitSymbol )})\n`; csv += "\n"; for (let i = 0; i < waveform.length; i++) { csv += numberFormat.format(i / waveform.samplingRate / 1000); csv += separator; const value = waveform.value(i); csv += numberFormat.format(value); csv += "\n"; if (i > 0 && i % 100000 === 0) { const progress = i / waveform.length; notification.update(progressToastId, { progress }); await new Promise(resolve => setTimeout(resolve, 0)); } } notification.dismiss(progressToastId); return csv; }