import React from "react";
import { observable, makeObservable, runInAction, autorun } from "mobx";
import { Stream } from "stream";
import { Dialog, showDialog } from "eez-studio-ui/dialog";
import {
parseScpi,
SCPI_PART_EXPR,
SCPI_PART_QUERY_WITH_ASSIGNMENT,
SCPI_PART_STRING
} from "eez-studio-shared/scpi-parser";
import type { InstrumentObject } from "instrument/instrument-object";
import type * as InstrumentObjectModule from "instrument/instrument-object";
import type { ConnectionParameters } from "instrument/connection/interface";
import type * as WaveformFormatModule from "eez-studio-ui/chart/WaveformFormat";
import type * as ActivityLogModule from "instrument/window/history/activity-log";
import type { IDashboardComponentContext } from "eez-studio-types";
import {
registerClass,
PropertyType,
makeDerivedClassInfo,
MessageType,
ProjectType,
IMessage
} from "project-editor/core/object";
import {
ActionComponent,
ComponentOutput,
makeExpressionProperty
} from "project-editor/flow/component";
import type { IFlowContext } from "project-editor/flow//flow-interfaces";
import { Assets, DataBuffer } from "project-editor/build/assets";
import {
getChildOfObject,
getProjectStore,
Message,
ProjectStore
} from "project-editor/store";
import { isNotDashboardProject } from "project-editor/project/project-type-traits";
import {
buildExpression,
buildAssignableExpression,
checkExpression,
checkAssignableExpression
} from "project-editor/flow/expression";
import {
IObjectVariableValue,
IObjectVariableValueConstructorParams,
isStructType,
registerObjectVariableType,
ValueType
} from "project-editor/features/variable/value-type";
import type { IVariable } from "project-editor/flow/flow-interfaces";
import { specificGroup } from "project-editor/ui-components/PropertyGrid/groups";
import { COMPONENT_TYPE_SCPI_ACTION } from "project-editor/flow/components/component-types";
import { ProjectContext } from "project-editor/project/context";
import { ProjectEditor } from "project-editor/project-editor-interface";
import { findVariable } from "project-editor/project/project";
import { Instruments, InstrumentsStore } from "home/instruments";
import { observer } from "mobx-react";
import { AlertDanger } from "eez-studio-ui/alert";
import { Loader } from "eez-studio-ui/loader";
import {
offWasmFlowRuntimeTerminate,
onWasmFlowRuntimeTerminate
} from "project-editor/flow/runtime/wasm-worker";
import { DashboardComponentContext } from "project-editor/flow/runtime/worker-dashboard-component-context";
import type { PlotlyLineChartExecutionState } from "../widgets/dashboard/plotly";
import type { TabulatorExecutionState } from "../widgets/dashboard/tabulator";
////////////////////////////////////////////////////////////////////////////////
export class SCPIActionComponent extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
flowComponentId: COMPONENT_TYPE_SCPI_ACTION,
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL,
properties: [
makeExpressionProperty(
{
name: "instrument",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: isNotDashboardProject
},
"object:Instrument"
),
makeExpressionProperty(
{
name: "scpi",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
monospaceFont: true,
flowProperty: "scpi-template-literal",
expressionType: undefined,
getInstrumentId: (component: SCPIActionComponent) => {
const projectStore =
ProjectEditor.getProjectStore(component);
const instrumentVariable = findVariable(
projectStore.project,
component.instrument
);
if (
!instrumentVariable ||
instrumentVariable.type != "object:Instrument"
) {
return undefined;
}
const value =
projectStore.runtimeSettings.getVariableValue(
instrumentVariable
);
if (
!value ||
value.id == undefined ||
typeof value.id != "string"
) {
return undefined;
}
return value.id;
}
},
"any"
),
makeExpressionProperty(
{
name: "timeout",
displayName: "Timeout (ms)",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"integer"
),
makeExpressionProperty(
{
name: "delay",
displayName: "Delay (ms)",
formText: "Minimum delay between commands.",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"integer"
)
],
beforeLoadHook: (component: SCPIActionComponent, jsObject: any) => {
if (jsObject.scpi) {
if (!jsObject.customInputs && !jsObject.customOutputs) {
jsObject.customInputs = [];
jsObject.customOutputs = [];
try {
const parts = parseScpi(jsObject.scpi);
for (const part of parts) {
const tag = part.tag;
const str = part.value!;
if (tag == SCPI_PART_EXPR) {
const inputName = str.substring(
1,
str.length - 1
);
if (
!jsObject.customInputs.find(
(customInput: {
name: string;
type: PropertyType;
}) => customInput.name == inputName
)
) {
jsObject.customInputs.push({
name: inputName,
type: "string"
});
}
} else if (tag == SCPI_PART_QUERY_WITH_ASSIGNMENT) {
const outputName =
str[0] == "{"
? str.substring(1, str.length - 1)
: str;
jsObject.customOutputs.push({
name: outputName,
type: "any"
});
}
}
} catch (err) {}
}
}
if (jsObject.timeout == undefined) {
jsObject.timeout = "null";
}
if (jsObject.delay == undefined) {
jsObject.delay = "null";
}
},
check: (component: SCPIActionComponent, messages: IMessage[]) => {
try {
const parts = parseScpi(component.scpi);
for (const part of parts) {
const tag = part.tag;
const str = part.value!;
if (tag == SCPI_PART_EXPR) {
try {
const expr = str.substring(1, str.length - 1);
checkExpression(component, expr);
} catch (err) {
messages.push(
new Message(
MessageType.ERROR,
`Invalid expression: ${err}`,
getChildOfObject(component, "scpi")
)
);
}
} else if (tag == SCPI_PART_QUERY_WITH_ASSIGNMENT) {
try {
const assignableExpression =
str[0] == "{"
? str.substring(1, str.length - 1)
: str;
checkAssignableExpression(
component,
assignableExpression
);
} catch (err) {
messages.push(
new Message(
MessageType.ERROR,
`Invalid assignable expression: ${err}`,
getChildOfObject(component, "scpi")
)
);
}
}
}
} catch (err) {
messages.push(
new Message(
MessageType.ERROR,
`Invalid SCPI: ${err}`,
getChildOfObject(component, "scpi")
)
);
}
},
label: (component: SCPIActionComponent) => {
const project = getProjectStore(component).project;
if (project.projectTypeTraits.isDashboard && component.instrument) {
return `SCPI ${component.instrument}`;
}
return "SCPI";
},
componentPaletteLabel: "SCPI",
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument"
});
instrument: string;
scpi: string;
timeout: string;
delay: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrument: observable,
scpi: observable,
timeout: observable,
delay: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: true
},
...super.getInputs()
];
}
getOutputs() {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
...super.getOutputs()
];
}
getBody(flowContext: IFlowContext): React.ReactNode {
return (
);
}
buildFlowComponentSpecific(assets: Assets, dataBuffer: DataBuffer) {
const parts = parseScpi(this.scpi);
for (const part of parts) {
dataBuffer.writeUint8(part.tag);
const str = part.value!;
if (part.tag == SCPI_PART_STRING) {
dataBuffer.writeUint16NonAligned(str.length);
for (const ch of str) {
dataBuffer.writeUint8(ch.codePointAt(0)!);
}
} else if (part.tag == SCPI_PART_EXPR) {
const expression = str.substring(1, str.length - 1);
buildExpression(assets, dataBuffer, this, expression);
} else if (part.tag == SCPI_PART_QUERY_WITH_ASSIGNMENT) {
const lValueExpression =
str[0] == "{" ? str.substring(1, str.length - 1) : str;
buildAssignableExpression(
assets,
dataBuffer,
this,
lValueExpression
);
}
}
}
}
registerClass("SCPIActionComponent", SCPIActionComponent);
////////////////////////////////////////////////////////////////////////////////
export class SelectInstrumentActionComponent extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [],
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
execute: async (context: IDashboardComponentContext) => {
try {
context.startAsyncExecution();
const instrument = await showSelectInstrumentDialog(
undefined,
undefined,
undefined,
false
);
context.endAsyncExecution();
if (instrument) {
context.propagateValue("instrument", instrument);
}
context.propagateValueThroughSeqout();
} catch (err) {
context.endAsyncExecution();
context.throwError(err.toString());
}
}
});
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs(): ComponentOutput[] {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
{
name: "instrument",
type: "object:Instrument",
isSequenceOutput: false,
isOptionalOutput: false
},
...super.getOutputs()
];
}
}
registerClass(
"SelectInstrumentActionComponent",
SelectInstrumentActionComponent
);
////////////////////////////////////////////////////////////////////////////////
export class GetInstrumentActionComponent extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [
makeExpressionProperty(
{
name: "instrumentId",
displayName: "Instrument ID",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"string"
)
],
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
execute: (context: IDashboardComponentContext) => {
const instrumentId = context.evalProperty("instrumentId");
if (instrumentId == undefined || typeof instrumentId != "string") {
context.throwError(`Invalid Instrument ID property`);
return;
}
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
const instrument = instruments.get(instrumentId);
if (!instrument) {
context.throwError("Instrument not found");
return;
}
context.propagateValue("instrument", instrument);
context.propagateValueThroughSeqout();
}
});
instrumentId: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrumentId: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs(): ComponentOutput[] {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
{
name: "instrument",
type: "object:Instrument",
isSequenceOutput: false,
isOptionalOutput: false
},
...super.getOutputs()
];
}
}
registerClass("GetInstrumentActionComponent", GetInstrumentActionComponent);
////////////////////////////////////////////////////////////////////////////////
export class ConnectInstrumentActionComponent extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [
makeExpressionProperty(
{
name: "instrument",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"object:Instrument"
)
],
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
execute: (context: IDashboardComponentContext) => {
interface InstrumentVariableTypeConstructorParams {
id: string;
}
const instrument =
context.evalProperty(
"instrument"
);
if (instrument == undefined || typeof instrument.id != "string") {
context.throwError(`Invalid instrument property`);
return;
}
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
const instrumentObject = instruments.get(instrument.id);
if (!instrumentObject) {
context.throwError(`Instrument not found`);
return;
}
instrumentObject.connection.connect();
context.propagateValueThroughSeqout();
}
});
instrument: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrument: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs() {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
...super.getOutputs()
];
}
getBody(flowContext: IFlowContext): React.ReactNode {
if (!this.instrument) {
return null;
}
if (this.customInputs.find(input => input.name == this.instrument)) {
return null;
}
return (
);
}
}
registerClass(
"ConnectInstrumentActionComponent",
ConnectInstrumentActionComponent
);
////////////////////////////////////////////////////////////////////////////////
export class DisconnectInstrumentActionComponent extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [
makeExpressionProperty(
{
name: "instrument",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"object:Instrument"
)
],
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
execute: (context: IDashboardComponentContext) => {
interface InstrumentVariableTypeConstructorParams {
id: string;
}
const instrument =
context.evalProperty(
"instrument"
);
if (instrument == undefined || typeof instrument.id != "string") {
context.throwError(`Invalid instrument property`);
return;
}
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
const instrumentObject = instruments.get(instrument.id);
if (!instrumentObject) {
context.throwError(`Instrument not found`);
return;
}
instrumentObject.connection.disconnect();
context.propagateValueThroughSeqout();
}
});
instrument: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrument: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs() {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
...super.getOutputs()
];
}
getBody(flowContext: IFlowContext): React.ReactNode {
if (!this.instrument) {
return null;
}
if (this.customInputs.find(input => input.name == this.instrument)) {
return null;
}
return (
);
}
}
registerClass(
"DisconnectInstrumentActionComponent",
DisconnectInstrumentActionComponent
);
////////////////////////////////////////////////////////////////////////////////
export class InstrumentRead extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [
makeExpressionProperty(
{
name: "instrument",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"object:Instrument"
)
],
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
execute: (context: IDashboardComponentContext) => {
interface InstrumentVariableTypeConstructorParams {
id: string;
}
const instrument =
context.evalProperty(
"instrument"
);
if (instrument == undefined || typeof instrument.id != "string") {
context.throwError(`Invalid instrument property`);
return;
}
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
const instrumentObject = instruments.get(instrument.id);
if (!instrumentObject) {
context.throwError(`Instrument not found`);
return;
}
context = context.startAsyncExecution();
const readableStream = new Stream.Readable();
readableStream._read = () => {};
readableStream.on("close", () => {
context.endAsyncExecution();
connection.offRead(onReadCallback);
});
context.propagateValue("data", readableStream);
const connection = instrumentObject.connection;
const onReadCallback = (data: string | undefined) => {
if (data) {
readableStream.push(data);
} else {
readableStream.destroy();
}
};
connection.onRead(onReadCallback);
const onTerminateCallback = () => {
offWasmFlowRuntimeTerminate(onTerminateCallback);
connection.offRead(onReadCallback);
};
onWasmFlowRuntimeTerminate(onTerminateCallback);
context.propagateValueThroughSeqout();
}
});
instrument: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrument: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs() {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
{
name: "data",
type: "stream" as ValueType,
isSequenceOutput: false,
isOptionalOutput: false
},
...super.getOutputs()
];
}
getBody(flowContext: IFlowContext): React.ReactNode {
if (!this.instrument) {
return null;
}
if (this.customInputs.find(input => input.name == this.instrument)) {
return null;
}
return (
);
}
}
registerClass("InstrumentRead", InstrumentRead);
////////////////////////////////////////////////////////////////////////////////
export class InstrumentWrite extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [
makeExpressionProperty(
{
name: "instrument",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"object:Instrument"
),
makeExpressionProperty(
{
name: "data",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"string"
)
],
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
execute: async (context: IDashboardComponentContext) => {
interface InstrumentVariableTypeConstructorParams {
id: string;
}
const instrument =
context.evalProperty(
"instrument"
);
if (instrument == undefined || typeof instrument.id != "string") {
context.throwError(`Invalid instrument property`);
return;
}
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
const instrumentObject = instruments.get(instrument.id);
if (!instrumentObject) {
context.throwError(`Instrument not found`);
return;
}
const data = context.evalProperty("data");
if (typeof data != "string") {
context.throwError(`Data is not a string`);
return;
}
context.startAsyncExecution();
const connection = instrumentObject.connection;
try {
await connection.acquire(false);
try {
await connection.command(data);
context.endAsyncExecution();
} finally {
connection.release();
}
} catch (err) {
context.endAsyncExecution();
context.throwError(err.toString());
}
context.propagateValueThroughSeqout();
}
});
instrument: string;
data: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrument: observable,
data: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs() {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
...super.getOutputs()
];
}
getBody(flowContext: IFlowContext): React.ReactNode {
if (!this.instrument) {
return null;
}
if (this.customInputs.find(input => input.name == this.instrument)) {
return null;
}
return (
{this.instrument}
{this.data}
);
}
}
registerClass("InstrumentWrite", InstrumentWrite);
////////////////////////////////////////////////////////////////////////////////
export class GetInstrumentPropertiesActionComponent extends ActionComponent {
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [
makeExpressionProperty(
{
name: "instrument",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"object:Instrument"
)
],
defaultValue: {
customOutputs: [
{
name: "properties",
type: "any"
}
]
},
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
check: (
component: GetInstrumentPropertiesActionComponent,
messages: IMessage[]
) => {
const output = component.customOutputs.find(
output => output.name == "properties"
);
if (output) {
if (!isStructType(output.type)) {
messages.push(
new Message(
MessageType.ERROR,
`Output "properties" must be of struct type`,
component
)
);
}
} else {
messages.push(
new Message(
MessageType.ERROR,
`Output "properties" not found`,
component
)
);
}
},
execute: (context: IDashboardComponentContext) => {
interface InstrumentVariableTypeConstructorParams {
id: string;
}
const instrument =
context.evalProperty(
"instrument"
);
if (instrument == undefined || typeof instrument.id != "string") {
context.throwError(`Invalid instrument property`);
return;
}
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
const instrumentObject = instruments.get(instrument.id);
if (!instrumentObject) {
context.throwError(`Instrument ${instrument.id} not found`);
return;
}
context.propagateValue("properties", instrumentObject.properties);
context.propagateValueThroughSeqout();
}
});
instrument: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrument: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs(): ComponentOutput[] {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
...super.getOutputs()
];
}
}
registerClass(
"GetInstrumentPropertiesActionComponent",
GetInstrumentPropertiesActionComponent
);
////////////////////////////////////////////////////////////////////////////////
export class AddToInstrumentHistoryActionComponent extends ActionComponent {
static ITEM_TYPE_NONE = 0;
static ITEM_TYPE_EEZ_CHART = 1;
static ITEM_TYPE_WIDGET = 2;
static classInfo = makeDerivedClassInfo(ActionComponent.classInfo, {
properties: [
makeExpressionProperty(
{
name: "instrument",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"object:Instrument"
),
{
name: "itemType",
type: PropertyType.Enum,
enumItems: [
{
id: "chart",
label: "EEZ-Chart"
},
{
id: "widget",
label: "Widget"
}
],
propertyGridGroup: specificGroup
},
makeExpressionProperty(
{
name: "chartDescription",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"string"
),
makeExpressionProperty(
{
name: "chartData",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"blob"
),
makeExpressionProperty(
{
name: "chartSamplingRate",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"float"
),
makeExpressionProperty(
{
name: "chartOffset",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"double"
),
makeExpressionProperty(
{
name: "chartScale",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"double"
),
makeExpressionProperty(
{
name: "chartFormat",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
},
formText: `"float", "double", "rigol-byte", "rigol-word", "csv"`
},
"string"
),
makeExpressionProperty(
{
name: "chartUnit",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
},
formText: `"voltage", "current", "watt", "power", "time", "frequency", "joule"`
},
"integer"
),
makeExpressionProperty(
{
name: "chartColor",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"string"
),
makeExpressionProperty(
{
name: "chartColorInverse",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"string"
),
makeExpressionProperty(
{
name: "chartLabel",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"string"
),
makeExpressionProperty(
{
name: "chartMajorSubdivisionHorizontal",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"integer"
),
makeExpressionProperty(
{
name: "chartMajorSubdivisionVertical",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"integer"
),
makeExpressionProperty(
{
name: "chartMinorSubdivisionHorizontal",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"integer"
),
makeExpressionProperty(
{
name: "chartMinorSubdivisionVertical",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"integer"
),
makeExpressionProperty(
{
name: "chartHorizontalScale",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"double"
),
makeExpressionProperty(
{
name: "chartVerticalScale",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "chart";
}
},
"double"
),
makeExpressionProperty(
{
name: "widget",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup,
disabled: (
component: AddToInstrumentHistoryActionComponent
) => {
return component.itemType != "widget";
}
},
"widget"
)
],
defaultValue: {},
beforeLoadHook: (
component: AddToInstrumentHistoryActionComponent,
jsObject: any
) => {
if (
jsObject.itemType == "plotly" ||
jsObject.itemType == "tabulator"
) {
jsObject.itemType = "widget";
}
if (jsObject.plotlyWidget != undefined) {
jsObject.widget = jsObject.plotlyWidget;
delete jsObject.plotlyWidget;
}
},
icon: (
),
componentHeaderColor: "#FDD0A2",
componentPaletteGroupName: "Instrument",
execute: (context: IDashboardComponentContext) => {
interface InstrumentVariableTypeConstructorParams {
id: string;
}
const instrument =
context.evalProperty(
"instrument"
);
if (instrument == undefined || typeof instrument.id != "string") {
context.throwError(`Invalid instrument property`);
return;
}
const itemType = context.getUint8Param(0);
let message;
let chartData;
let historyItemType;
if (
itemType ==
AddToInstrumentHistoryActionComponent.ITEM_TYPE_EEZ_CHART
) {
const chartDescription =
context.evalProperty("chartDescription");
if (chartDescription == undefined) {
context.throwError(`Invalid Chart description property`);
return;
}
const chartData = context.evalProperty("chartData");
if (chartData == undefined) {
context.throwError(`Invalid Chart data property`);
return;
}
const chartSamplingRate =
context.evalProperty("chartSamplingRate");
if (chartSamplingRate == undefined) {
context.throwError(`Invalid Chart sampling rate property`);
return;
}
const chartOffset = context.evalProperty("chartOffset");
if (chartOffset == undefined) {
context.throwError(`Invalid Chart offet property`);
return;
}
const chartScale = context.evalProperty("chartScale");
if (chartScale == undefined) {
context.throwError(`Invalid Chart scale property`);
return;
}
const chartFormatStr =
context.evalProperty("chartFormat");
if (chartFormatStr == undefined) {
context.throwError(`Invalid Chart format property`);
return;
}
const { WaveformFormat } =
require("eez-studio-ui/chart/WaveformFormat") as typeof WaveformFormatModule;
let chartFormat =
chartFormatStr == "float"
? WaveformFormat.FLOATS_32BIT
: chartFormatStr == "double"
? WaveformFormat.FLOATS_64BIT
: chartFormatStr == "rigol-byte"
? WaveformFormat.RIGOL_BYTE
: chartFormatStr == "rigol-word"
? WaveformFormat.RIGOL_WORD
: chartFormatStr == "csv"
? WaveformFormat.CSV_STRING
: WaveformFormat.JS_NUMBERS;
const chartUnit = context.evalProperty("chartUnit");
if (chartUnit == undefined) {
context.throwError(`Invalid Chart unit property`);
return;
}
const chartColor = context.evalProperty("chartColor");
if (chartColor == undefined) {
context.throwError(`Invalid Chart color property`);
return;
}
const chartColorInverse =
context.evalProperty("chartColorInverse");
if (chartColorInverse == undefined) {
context.throwError(`Invalid Chart color inverse property`);
return;
}
const chartLabel = context.evalProperty("chartLabel");
if (chartLabel == undefined) {
context.throwError(`Invalid Chart label property`);
return;
}
const chartMajorSubdivisionHorizontal =
context.evalProperty(
"chartMajorSubdivisionHorizontal"
);
if (chartMajorSubdivisionHorizontal == undefined) {
context.throwError(
`Invalid Chart major subdivision horizontal property`
);
return;
}
const chartMajorSubdivisionVertical =
context.evalProperty(
"chartMajorSubdivisionVertical"
);
if (chartMajorSubdivisionVertical == undefined) {
context.throwError(
`Invalid Chart major subdivision vertical property`
);
return;
}
const chartMinorSubdivisionHorizontal =
context.evalProperty(
"chartMinorSubdivisionHorizontal"
);
if (chartMinorSubdivisionHorizontal == undefined) {
context.throwError(
`Invalid Chart minor subdivision horizontal property`
);
return;
}
const chartMinorSubdivisionVertical =
context.evalProperty(
"chartMinorSubdivisionVertical"
);
if (chartMinorSubdivisionVertical == undefined) {
context.throwError(
`Invalid Chart minor subdivision vertical property`
);
return;
}
const chartHorizontalScale = context.evalProperty(
"chartHorizontalScale"
);
if (chartHorizontalScale == undefined) {
context.throwError(
`Invalid Chart horizontal scale property`
);
return;
}
const chartVerticalScale =
context.evalProperty("chartVerticalScale");
if (chartVerticalScale == undefined) {
context.throwError(`Invalid Chart vertical scale property`);
return;
}
const message: any = {
state: "success",
fileType: { mime: "application/eez-raw" },
description: chartDescription,
waveformDefinition: {
samplingRate: chartSamplingRate,
format: chartFormat,
unitName: chartUnit.toLowerCase(),
color: chartColor,
colorInverse: chartColorInverse,
label: chartLabel,
offset: chartOffset,
scale: chartScale
},
viewOptions: {
axesLines: {
type: "fixed",
majorSubdivision: {
horizontal: chartMajorSubdivisionHorizontal,
vertical: chartMajorSubdivisionVertical
},
minorSubdivision: {
horizontal: chartMinorSubdivisionHorizontal,
vertical: chartMinorSubdivisionVertical
}
}
},
horizontalScale: chartHorizontalScale,
verticalScale: chartVerticalScale
};
message.dataLength = chartData.length;
historyItemType = "instrument/file-download";
} else if (
itemType ==
AddToInstrumentHistoryActionComponent.ITEM_TYPE_WIDGET
) {
const widget = context.evalProperty("widget");
if (widget == undefined) {
context.throwError(`Invalid widget property`);
return;
}
const widgetInfo =
context.WasmFlowRuntime.getWidgetHandleInfo(widget);
if (!widgetInfo) {
context.throwError(`Invalid widget handle`);
return;
}
const widgetContext = new DashboardComponentContext(
context.WasmFlowRuntime,
widgetInfo.flowStateIndex,
widgetInfo.componentIndex
);
const executionState = widgetContext.getComponentExecutionState<
PlotlyLineChartExecutionState | TabulatorExecutionState
>();
if (!executionState || !executionState.getInstrumentItemData) {
context.throwError(`Invalid Plotly widget execution state`);
return;
}
const instrumentItemData =
executionState.getInstrumentItemData();
historyItemType = instrumentItemData.itemType;
message = instrumentItemData.message;
chartData = undefined;
} else {
context.throwError("Invalid item type");
return;
}
const { activityLogStore, log } =
require("instrument/window/history/activity-log") as typeof ActivityLogModule;
const historyId = log(
activityLogStore,
{
oid: instrument.id,
type: historyItemType,
message: JSON.stringify(message),
data: chartData,
temporary: false
},
{
undoable: false
}
);
context.propagateValue("id", historyId);
context.propagateValueThroughSeqout();
}
});
instrument: string;
itemType: string;
chartDescription: string;
chartData: string;
chartSamplingRate: string;
chartOffset: string;
chartScale: string;
chartFormat: string;
chartUnit: string;
chartColor: string;
chartColorInverse: string;
chartLabel: string;
chartMajorSubdivisionHorizontal: string;
chartMajorSubdivisionVertical: string;
chartMinorSubdivisionHorizontal: string;
chartMinorSubdivisionVertical: string;
chartHorizontalScale: string;
chartVerticalScale: string;
widget: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
instrument: observable,
itemType: observable,
chartDescription: observable,
chartData: observable,
chartSamplingRate: observable,
chartOffset: observable,
chartScale: observable,
chartFormat: observable,
chartUnit: observable,
chartColor: observable,
chartColorInverse: observable,
chartLabel: observable,
chartMajorSubdivisionHorizontal: observable,
chartMajorSubdivisionVertical: observable,
chartMinorSubdivisionHorizontal: observable,
chartMinorSubdivisionVertical: observable,
chartHorizontalScale: observable,
chartVerticalScale: observable,
widget: observable
});
}
getInputs() {
return [
{
name: "@seqin",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: false
},
...super.getInputs()
];
}
getOutputs(): ComponentOutput[] {
return [
{
name: "@seqout",
type: "null" as ValueType,
isSequenceOutput: true,
isOptionalOutput: true
},
{
name: "id",
type: "string",
isSequenceOutput: false,
isOptionalOutput: true
},
...super.getOutputs()
];
}
buildFlowComponentSpecific(assets: Assets, dataBuffer: DataBuffer) {
if (this.itemType == "chart") {
dataBuffer.writeUint8(
AddToInstrumentHistoryActionComponent.ITEM_TYPE_EEZ_CHART
);
} else if (this.itemType == "widget") {
dataBuffer.writeUint8(
AddToInstrumentHistoryActionComponent.ITEM_TYPE_WIDGET
);
} else {
dataBuffer.writeUint8(
AddToInstrumentHistoryActionComponent.ITEM_TYPE_NONE
);
}
}
}
registerClass(
"AddToInstrumentHistoryActionComponent",
AddToInstrumentHistoryActionComponent
);
////////////////////////////////////////////////////////////////////////////////
const Connection = observer(
class Connection extends React.Component<{
instrumentsStore: InstrumentsStore;
}> {
dismissError = () => {
const instrument = this.props.instrumentsStore.selectedInstrument;
if (!instrument) {
return;
}
instrument.connection.dismissError();
};
render() {
const instrument = this.props.instrumentsStore.selectedInstrument;
if (!instrument) {
return null;
}
let connection = instrument.connection;
let info;
let error;
let connectionParameters;
let abort;
if (connection) {
if (connection.isIdle) {
error = connection.error && (
{connection.error}
);
const { ConnectionProperties } =
require("instrument/window/connection-dialog") as typeof import("instrument/window/connection-dialog");
connectionParameters = (
{
this.props.instrumentsStore.connectionParameters =
connectionParameters;
}}
availableConnections={
instrument.availableConnections
}
serialBaudRates={instrument.serialBaudRates}
/>
);
} else {
if (connection.isTransitionState) {
info = ;
}
const { ConnectionParametersDetails } =
require("home/instruments/instrument-object-details") as typeof import("home/instruments/instrument-object-details");
connectionParameters = (
);
abort = (
);
}
}
return (
{info}
{error}
{connectionParameters}
{abort}
);
}
}
);
export const SelectInstrumentDialog = observer(
class SelectInstrumentDialog extends React.Component<{
projectStore?: ProjectStore;
instrumentsStore: InstrumentsStore;
selectAndConnect?: boolean;
resolve: (instrument: InstrumentObject | undefined) => void;
}> {
connectToInstrument: boolean = false;
connectionParameters: ConnectionParameters | null;
dispose: any;
constructor(props: any) {
super(props);
makeObservable(this, {
connectToInstrument: observable
});
}
componentDidMount() {
this.dispose = autorun(() => {
if (
this.connectToInstrument &&
this.props.instrumentsStore.selectedInstrument &&
this.props.instrumentsStore.selectedInstrument.isConnected
) {
this.props.resolve(
this.props.instrumentsStore.selectedInstrument
);
}
});
this.props.instrumentsStore.onSelectInstrument = this.onOk;
}
componentWillUnmount() {
this.dispose();
this.props.instrumentsStore.onSelectInstrument = undefined;
}
onOk = () => {
const instrument = this.props.instrumentsStore.selectedInstrument;
if (instrument) {
if (this.connectToInstrument) {
this.props.instrumentsStore.selectedInstrumentConnect();
} else {
if (
this.props.selectAndConnect &&
!instrument.isConnected
) {
runInAction(() => (this.connectToInstrument = true));
} else {
this.props.resolve(instrument);
}
}
}
};
render() {
const instrument = this.props.instrumentsStore.selectedInstrument;
let content;
if (this.connectToInstrument && instrument) {
content = (
{instrument.connection?.isIdle
? `Connect to ${instrument.name}`
: `Connecting to ${instrument.name} ...`}
);
} else {
content = (
);
}
const dialog = (
);
if (this.props.projectStore) {
return (
{dialog}
);
} else {
return dialog;
}
}
}
);
export async function showSelectInstrumentDialog(
projectStore: ProjectStore | undefined,
name?: string,
instrumentId?: string | undefined,
selectAndConnect?: boolean
) {
return new Promise(resolve => {
const instrumentsStore = new InstrumentsStore(true);
instrumentsStore.selectedInstrumentId = instrumentId;
const [modalDialog] = showDialog(
{
if (instrument) {
resolve(instrument);
}
modalDialog.close();
}}
/>,
{
jsPanel: {
id: "select-instrument-5",
title: name ? `Select: ${name}` : "Select Instrument",
width: 920,
height: 680
}
}
);
});
}
////////////////////////////////////////////////////////////////////////////////
async function connectToInstrument(instrument: InstrumentObject) {
if (!instrument.lastConnection) {
return;
}
const connection = instrument.connection;
connection.connect();
for (let i = 0; i < 10; i++) {
try {
await connection.acquire(false);
connection.release();
return;
} catch (err) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
}
type InstrumentConstructorParams = "string" | { id: "string" };
function getInstrumentIdFromConstructorParams(
constructorParams: InstrumentConstructorParams | undefined
) {
return constructorParams == null
? null
: typeof constructorParams === "string"
? constructorParams
: (constructorParams.id as string);
}
registerObjectVariableType("Instrument", {
editConstructorParams: async (
variable: IVariable,
constructorParams?: InstrumentConstructorParams,
runtime?: boolean
): Promise => {
let instrument = await showSelectInstrumentDialog(
ProjectEditor.getProjectStore(variable as any),
variable.description || variable.fullName,
getInstrumentIdFromConstructorParams(constructorParams) ??
undefined,
!(runtime === false)
);
return instrument
? {
id: instrument.id
}
: undefined;
},
createValue: (
constructorParams: InstrumentConstructorParams,
isRuntime: boolean
) => {
const instrumentId =
getInstrumentIdFromConstructorParams(constructorParams);
if (instrumentId) {
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
const instrument = instruments.get(instrumentId);
if (instrument) {
if (isRuntime) {
connectToInstrument(instrument);
}
return instrument;
}
}
return {
constructorParams,
status: {
label: "Unknown instrument",
error: `Instrument with ID [${constructorParams}] is not found`
}
};
},
destroyValue: (value: IObjectVariableValue) => {},
getValue: (variableValue: any): IObjectVariableValue | null => {
const { instruments } =
require("instrument/instrument-object") as typeof InstrumentObjectModule;
return instruments.get(variableValue.id) ?? null;
},
valueFieldDescriptions: [
{
name: "id",
valueType: "string",
getFieldValue: (value: InstrumentObject): string => {
return value.id;
}
},
{
name: "isConnected",
valueType: "boolean",
getFieldValue: (value: InstrumentObject): boolean => {
return value.isConnected;
}
}
]
});