import React from "react"; import { observable, computed, reaction, toJS, runInAction, makeObservable, IReactionDisposer } from "mobx"; import classNames from "classnames"; import { objectEqual, formatDateTimeLong } from "eez-studio-shared/util"; import { capitalize } from "eez-studio-shared/string"; import { TIME_UNIT, FREQUENCY_UNIT, UNITS, IUnit } from "eez-studio-shared/units"; import { Point } from "eez-studio-shared/geometry"; import { IStore } from "eez-studio-shared/store"; import { readBinaryFile } from "eez-studio-shared/util-electron"; import { ChartController, IChartController, ChartMode, IAxisModel, ZoomMode, LineController, IAxisController, IMeasurementsModel } from "eez-studio-ui/chart/chart"; import { AxisController, ChartsController, MeasurementsModel, IWaveform, getNearestValuePoint, WaveformModel } from "eez-studio-ui/chart/chart"; import { RulersModel, IRulersModel } from "eez-studio-ui/chart/rulers"; import { WaveformLineView } from "eez-studio-ui/chart/WaveformLineView"; import { DataType } from "eez-studio-ui/chart/DataType"; import { WaveformFormat } from "eez-studio-ui/chart/WaveformFormat"; import { initValuesAccesor } from "eez-studio-ui/chart/value-accesor"; import { logUpdate, IActivityLogEntry, log } from "instrument/window/history/activity-log"; import type { InstrumentAppStore } from "instrument/window/app-store"; import { ChartPreview } from "instrument/window/chart-preview"; import { MIME_EEZ_DLOG } from "instrument/connection/file-type"; import { FileHistoryItem } from "instrument/window/history/items/file"; import { ViewOptions } from "instrument/window/waveform/ViewOptions"; import { WaveformTimeAxisModel } from "instrument/window/waveform/time-axis"; import { IToolbarOptions, WaveformToolbar } from "instrument/window/waveform/toolbar"; import { IDlog, IDlogYAxis, decodeDlog, ScaleType, EMPTY_DLOG } from "instrument/window/waveform/dlog-file"; import { convertDlogToCsv, dlogUnitToStudioUnit } from "instrument/connection/file-type-utils"; import type { ChartsDisplayOption } from "instrument/window/lists/common-tools"; import type { IAppStore } from "instrument/window/history/history"; import { globalViewOptions } from "eez-studio-ui/chart/GlobalViewOptions"; //////////////////////////////////////////////////////////////////////////////// class DlogWaveformAxisModel implements IAxisModel { constructor( public yAxis: IDlogYAxis, public semiLogarithmic?: { a: number; b: number }, private yAxes?: IDlogYAxis[], private chartsController?: ChartsController ) { makeObservable(this, { dynamic: observable, fixed: observable, label: computed, color: computed, colorInverse: computed }); } get unit() { return this.yAxis.unit; } get minValue() { if (this.semiLogarithmic) { return 0; } if (this.yAxis.range) { return this.yAxis.range.min; } return 0; } get maxValue() { if (this.semiLogarithmic) { const logOffset = 1 - this.yAxis.range!.min; return Math.log10(logOffset + this.yAxis.range!.max); } if (this.yAxis.range) { return this.yAxis.range.max; } if (this.yAxis.unit.name === "voltage") { return 40; } if (this.yAxis.unit.name === "current") { return 5; } return 200; } get defaultFrom() { return this.minValue; } get defaultTo() { return this.maxValue; } dynamic: { zoomMode: ZoomMode; from: number; to: number; } = { zoomMode: "all", from: 0, to: 0 }; fixed: { zoomMode: ZoomMode; subdivisionOffset: number; subdivisonScale: number; } = { zoomMode: "all", subdivisionOffset: 0, subdivisonScale: 0 }; defaultSubdivisionOffset: number | undefined = undefined; defaultSubdivisionScale: number | undefined = undefined; get label() { function getLabel(yAxis: IDlogYAxis) { return yAxis.label ? yAxis.label : `Channel ${yAxis.channelIndex + 1} ${capitalize( yAxis.unit.name )}`; } if (this.yAxes) { return this.yAxes.map(getLabel).join(", "); } return getLabel(this.yAxis); } get labelReactNode(): React.ReactNode | undefined { if (!this.yAxes || !this.chartsController) { return undefined; } return (
{this.yAxes.map((yAxis, i) => (
{yAxis.label}
))}
); } get color() { return this.yAxes && this.yAxes.length > 1 ? "#fff" : this.yAxis.color ?? this.yAxis.unit.color; } get colorInverse() { return this.yAxes && this.yAxes.length > 1 ? "#333" : this.yAxis.colorInverse ?? this.yAxis.unit.colorInverse; } } //////////////////////////////////////////////////////////////////////////////// class TransformedAxisController extends AxisController { constructor(axisController: IAxisController, from: number, to: number) { super( axisController.position, axisController.chartsController, axisController.chartController, axisController.axisModel ); this._from = from; this._to = to; } _from: number; _to: number; get from() { return this._from; } get to() { return this._to; } get ticks() { return []; } panByDirection(direction: number): void {} panTo(to: number): void {} zoomAll(): void {} zoomDefault(): void {} zoomIn(): void {} zoomOut(): void {} zoom(from: number, to: number): void {} zoomAroundPivotPoint(pivotPx: number, zoomIn: boolean): void {} } //////////////////////////////////////////////////////////////////////////////// class DlogWaveformLineController extends LineController { constructor( public id: string, public dlogWaveform: DlogWaveform, yAxisController: IAxisController, private channel: IChannel, values: any, dataOffset: number, private channelsGroup: IChannelsGroup ) { super(id, yAxisController); makeObservable(this, { yAxisController: computed, yMin: computed, yMax: computed }); const yAxisIndex = this.dlogWaveform.channels.indexOf(channel); const columnDataIndex = dlogWaveform.dlog.columnDataIndexes[yAxisIndex]; const numBytesPerRow = dlogWaveform.dlog.numBytesPerRow; const length = dlogWaveform.dlog.length; this.waveform = { format: dlogWaveform.dlog.yAxisScaleType === ScaleType.LINEAR ? WaveformFormat.EEZ_DLOG : WaveformFormat.EEZ_DLOG_LOGARITHMIC, values, offset: 0, scale: 1, dlog: { dataType: dlogWaveform.dlog.yAxes[yAxisIndex].dataType, dataOffset, dataContainsSampleValidityBit: dlogWaveform.dlog.dataContainsSampleValidityBit, columnDataIndex, numBytesPerRow, bitMask: dlogWaveform.dlog.columnBitMask[yAxisIndex], logOffset: channel.yAxis.range ? 1 - channel.yAxis.range.min : 0, transformOffset: dlogWaveform.dlog.yAxes[yAxisIndex].transformOffset, transformScale: dlogWaveform.dlog.yAxes[yAxisIndex].transformScale }, length, value: undefined as any, waveformData: undefined as any, samplingRate: this.dlogWaveform.samplingRate, valueUnit: yAxisController.unit.name as keyof typeof UNITS }; initValuesAccesor(this.waveform); } waveform: IWaveform & { valueUnit: keyof typeof UNITS; }; get yAxisController() { const yAxisController = super.yAxisController; if (this.channel.yAxis.dataType === DataType.DATA_TYPE_BIT) { const offset = 1.0 - (this.channelsGroup.channels.indexOf(this.channel) + 1) / this.channelsGroup.channels.length + 1 / 20; const scale = 1.0 / this.channelsGroup.channels.length - 2 / 20; const from = (yAxisController.from - offset) / scale; const to = (yAxisController.to - offset) / scale; return new TransformedAxisController(yAxisController, from, to); } return yAxisController; } get yMin(): number { return this.yAxisController.axisModel.minValue; } get yMax(): number { return this.yAxisController.axisModel.maxValue; } get label() { return this.channel.axisModel.label; } getWaveformModel(): WaveformModel { return this.waveform; } getNearestValuePoint(point: Point): Point { return getNearestValuePoint( point, this.xAxisController, this.yAxisController, this.waveform ); } render(): JSX.Element { return ( ); } } //////////////////////////////////////////////////////////////////////////////// class DlogWaveformChartsController extends ChartsController { constructor( public dlogWaveform: DlogWaveform, mode: ChartMode, xAxisModel: IAxisModel ) { super(mode, xAxisModel, dlogWaveform.viewOptions); } get chartViewOptionsProps() { return { showRenderAlgorithm: true, showShowSampledDataOption: false }; } get supportRulers() { return true; } get bookmarks() { return this.dlogWaveform.dlog.bookmarks.length > 0 ? this.dlogWaveform.dlog.bookmarks : undefined; } } //////////////////////////////////////////////////////////////////////////////// interface IDlogChart {} interface IChannel { yAxis: IDlogYAxis; axisModel: IAxisModel; } interface IChannelsGroup { id: string; channels: IChannel[]; } export class DlogWaveform extends FileHistoryItem { dispose1: IReactionDisposer | undefined; dispose2: IReactionDisposer | undefined; dispose3: IReactionDisposer | undefined; dispose4: IReactionDisposer | undefined; constructor( store: IStore, activityLogEntry: IActivityLogEntry | FileHistoryItem, private options?: { toolbar?: IToolbarOptions } ) { super(store, activityLogEntry); makeObservable(this, { values: computed, dlog: computed, version: computed, xAxisUnit: computed, xAxisLabel: computed, samplingRate: computed, startTime: computed, channels: computed, hasJitterColumn: computed, length: computed, dataOffset: computed, charts: observable, channelsGroups: computed, xAxisModel: computed }); const message = JSON.parse(this.message); this.viewOptions = new ViewOptions(message.viewOptions); this.rulers = new RulersModel(message.rulers); this.measurements = new MeasurementsModel(message.measurements); // save viewOptions when changed this.dispose1 = reaction( () => toJS(this.viewOptions), viewOptions => { const message = JSON.parse(this.message); if (!objectEqual(message.viewOptions, viewOptions)) { logUpdate( this.store, { id: this.id, oid: this.oid, message: JSON.stringify( Object.assign(message, { viewOptions }) ) }, { undoable: false } ); } } ); // save rulers when changed this.dispose2 = 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.dispose3 = 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 } ); } } ); // make sure there is one Y ruler for each chart this.dispose4 = reaction( () => this.channels.length, numCharts => this.rulers.initYRulers(this.channels.length) ); } get values() { if (!this.transferSucceeded) { return undefined; } if (typeof this.data === "string") { return new Uint8Array(Buffer.from(this.data, "binary").buffer); } return this.data; } get dlog(): IDlog { return ( (this.values && decodeDlog(this.values, dlogUnitToStudioUnit)) || EMPTY_DLOG ); } get version() { return this.dlog.version; } get xAxisUnit() { return this.dlog.xAxis.unit; } get xAxisLabel() { return this.dlog.xAxis.label; } get samplingRate() { return 1 / this.dlog.xAxis.step; } get startTime() { return this.dlog.startTime; } get channels() { return this.dlog.yAxes.map(yAxis => ({ yAxis, axisModel: new DlogWaveformAxisModel(yAxis) })) as IChannel[]; } get hasJitterColumn() { return this.dlog.hasJitterColumn; } get length() { return this.dlog.length; } get dataOffset() { return this.dlog.dataOffset; } charts: IDlogChart = []; get description() { if (!this.values) { return null; } const step = this.dlog.xAxis.step; const stepStr = this.dlog.xAxis.unit.formatValue(step, 4); const sampleRate = 1 / this.dlog.xAxis.step; const sampleRateStr = FREQUENCY_UNIT.formatValue(sampleRate, 1); const max = (this.length - 1) * step; const maxStr = this.dlog.xAxis.unit.formatValue(max, 4); let info; if (this.dlog.xAxis.unit.name === TIME_UNIT.name) { info = `Period: ${stepStr}, Sample rate: ${sampleRateStr}, Duration: ${maxStr}`; } else { info = `Step: ${stepStr}, Max: ${maxStr}`; } if (this.startTime) { info = `Start time: ${formatDateTimeLong(this.startTime)}, ${info}`; } return
{info}
; } createLineController( chartController: IChartController, channel: IChannel, channelsGroup: IChannelsGroup ) { return new DlogWaveformLineController( "waveform-" + chartController.yAxisController.position + "-" + this.channels.indexOf(channel), this, chartController.yAxisController, channel, this.values || "", this.dataOffset, channelsGroup ); } get channelsGroups(): IChannelsGroup[] { const channelsGroups: IChannelsGroup[] = []; function compareYAxis( yAxis1: IDlogYAxis, yAxis2: IDlogYAxis ) { if (yAxis1.unit != yAxis2.unit) { return false; } if (yAxis1.range && yAxis2.range) { if ( yAxis1.range.min != yAxis2.range.min || yAxis1.range.max != yAxis2.range.max ) { return false; } } else if (yAxis1.range || yAxis2.range) { return false; } return true; } function findChannelsGroup(yAxis: IDlogYAxis) { for ( let channelsGroupIndex = 0; channelsGroupIndex < channelsGroups.length; channelsGroupIndex++ ) { const channelsGroup = channelsGroups[channelsGroupIndex]; if (compareYAxis(yAxis, channelsGroup.channels[0].yAxis)) { return channelsGroup; } } return undefined; } for ( let yAxisIndex = 0; yAxisIndex < this.dlog.yAxes.length; yAxisIndex++ ) { const yAxis = this.dlog.yAxes[yAxisIndex]; const channelsGroup = findChannelsGroup(yAxis); const channel = this.channels[yAxisIndex]; if (channelsGroup) { channelsGroup.channels.push(channel); } else { channelsGroups.push({ id: `dlog_chart_controller_${yAxisIndex}`, channels: [channel] }); } } return channelsGroups; } createChartControllerForChannelsGroup( chartsController: ChartsController, channelsGroup: IChannelsGroup ) { const chartController = new ChartController( chartsController, channelsGroup.id ); const yAxis = channelsGroup.channels[0].yAxis; chartController.createYAxisController( new DlogWaveformAxisModel( yAxis, this.dlog.yAxisScaleType == ScaleType.LOGARITHMIC ? { a: 0, b: -(1 - yAxis.range!.min) } : undefined, channelsGroup.channels.map(channel => channel.yAxis), chartsController ) ); if (yAxis.dataType === DataType.DATA_TYPE_BIT) { chartController.yAxisController.isDigital = true; } channelsGroup.channels.forEach(channel => { chartController.lineControllers.push( this.createLineController( chartController, channel, channelsGroup ) ); }); return chartController; } viewOptions: ViewOptions; rulers: IRulersModel; measurements: IMeasurementsModel; get xAxisModel() { return new WaveformTimeAxisModel( this, this.dlog.xAxis.scaleType === ScaleType.LOGARITHMIC ? { a: this.dlog.xAxis.range.min, b: 0 } : undefined ); } chartsController: ChartsController; createChartsController( appStore: IAppStore, displayOption: ChartsDisplayOption, mode: ChartMode ): ChartsController { if ( this.direction != "plotter" && this.chartsController && this.chartsController.mode === mode && this.chartsController.chartControllers && this.chartsController.chartControllers.length === this.channels.length ) { return this.chartsController; } if (this.chartsController) { this.chartsController.destroy(); } const chartsController = new DlogWaveformChartsController( this, mode, this.xAxisModel ); chartsController.chartViewWidth = this.chartsController?.chartViewWidth; chartsController.chartViewHeight = this.chartsController?.chartViewHeight; this.chartsController = chartsController; this.xAxisModel.chartsController = chartsController; chartsController.chartControllers = this.channelsGroups.map( channelsGroup => this.createChartControllerForChannelsGroup( chartsController, channelsGroup ) ); chartsController.createRulersController(this.rulers); chartsController.createMeasurementsController(this.measurements); return chartsController; } renderToolbar(chartsController: ChartsController): JSX.Element { return ( ); } openConfigurationDialog = undefined; get xAxisDefaultSubdivisionOffset(): number | undefined { return undefined; } get xAxisDefaultSubdivisionScale(): number | undefined { return undefined; } get yAxisDefaultSubdivisionOffset(): number | undefined { return undefined; } get yAxisDefaultSubdivisionScale(): number | undefined { return undefined; } getPreviewElement(appStore: IAppStore) { return ( ); } get isZoomable() { return this.state != "live"; } convertToCsv = async () => { return convertDlogToCsv(this.dlog); }; override dispose() { super.dispose(); if (this.dispose1) { this.dispose1(); } if (this.dispose2) { this.dispose2(); } if (this.dispose3) { this.dispose3(); } if (this.dispose4) { this.dispose4(); } } } //////////////////////////////////////////////////////////////////////////////// export async function importDlog( appStore: InstrumentAppStore, filePath: string ) { if (!filePath.toLowerCase().endsWith(".dlog")) { return false; } const data = await readBinaryFile(filePath); const dlog = decodeDlog(data, unit => unit); if (!dlog) { return false; } log( appStore.history.options.store, { oid: appStore.history.oid, type: "instrument/file-attachment", message: JSON.stringify({ sourceFilePath: filePath, state: "success", fileType: { ext: "dlog", mime: MIME_EEZ_DLOG }, dataLength: data.length, note: dlog.comment ? JSON.stringify([{ insert: dlog.comment }]) : undefined }), data }, { undoable: true, transaction: "Add DLOG chart" } ); appStore.navigationStore.navigateToHistory(); return false; }