Skip to content

Commit

Permalink
fix MultiStateValueDiscrete behavior #1323
Browse files Browse the repository at this point in the history
  • Loading branch information
erossignon committed Dec 3, 2023
1 parent 3c7c80b commit 77f1bf6
Show file tree
Hide file tree
Showing 11 changed files with 800 additions and 424 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { EnumValueTypeOptionsLike } from "../address_space_ts";
export function coerceEnumValues(enumValues: EnumValueTypeOptionsLike[] | { [key: string]: number | Int64 }): EnumValueType[] {
if (Array.isArray(enumValues)) {
//
return enumValues.map((en: any) => {
return enumValues.map((en) => {
assert(Object.prototype.hasOwnProperty.call(en, "value"));
assert(Object.prototype.hasOwnProperty.call(en, "displayName"));
return new EnumValueType({
displayName: coerceLocalizedText(en.displayName),
value: coerceInt64(en.value)
value: coerceInt64(en.value || 0)
});
});
} else {
Expand Down
2 changes: 2 additions & 0 deletions packages/node-opcua-address-space/source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export * from "./interfaces/alarms_and_conditions/ua_non_exclusive_limit_alarm_e
export { promoteToMultiStateDiscrete } from "../src/data_access/ua_multistate_discrete_impl";
export { promoteToMultiStateValueDiscrete } from "../src/data_access/ua_multistate_value_discrete_impl";
export { promoteToTwoStateDiscrete } from "../src/data_access/ua_two_state_discrete_impl";
export { validateIsNumericDataType } from "../src/data_access/ua_multistate_value_discrete_impl";
// deprecated: validateDataType
export { validateDataType } from "../src/data_access/ua_multistate_value_discrete_impl";
export { validateDataTypeCorrectness } from "../src/validate_data_type_correctness";
export * from "./ua_root_folder";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { DataType, Variant } from "node-opcua-variant";
* @module node-opcua-address-space.DataAccess
*/
import { StatusCode } from "node-opcua-status-code";
import { UAMultiStateDiscrete_Base } from "node-opcua-nodeset-ua";
import { UAVariableT } from "node-opcua-address-space-base";
import { DTEnumValue, UADiscreteItem, UADiscreteItem_Base, UAMultiStateDiscrete_Base } from "node-opcua-nodeset-ua";
import { UAProperty, UAVariableT } from "node-opcua-address-space-base";
import { Int64, UInt64 } from "node-opcua-basic-types";
import { LocalizedText } from "node-opcua-data-model";

export { UAMultiStateDiscrete } from "node-opcua-nodeset-ua";

Expand All @@ -24,3 +26,36 @@ export interface UAMultiStateDiscreteEx<T, DT extends DataType> extends UAVariab
setValue(value: string | number): void;
checkVariantCompatibility(value: Variant): StatusCode;
}







export interface UAMultiStateValueDiscreteArray_Base<T, DT extends DataType> extends UADiscreteItem_Base<T, DT> {
enumValues: UAProperty<DTEnumValue[], DataType.ExtensionObject>;
valueAsText: UAProperty<LocalizedText[], DataType.LocalizedText>;
}


export interface UAMultiStateValueDiscreteArray<T, DT extends DataType>
extends UADiscreteItem<T, DT>,
UAMultiStateValueDiscreteArray_Base<T, DT> {}

export interface UAMultiStateValueDiscreteArrayEx<T, DT extends DataType>
extends UAVariableT<T, DT>,
UAMultiStateValueDiscreteArray_Base<T, DT> {
/**
* EnumValues is an array of EnumValueType. Each entry of the array represents one enumeration
* value with its integer notation, a human-readable representation, and help information.
* This represents enumerations with integers that are not zero-based or have gaps (e.g. 1, 2, 4, 8, 16).
* See OPC 10000-3 for the definition of this type. MultiStateValueDiscrete Variables expose the
* current integer notation in their Value Attribute. Clients will often read the EnumValues
* Property in advance and cache it to lookup a name or help whenever they receive the numeric representation.
*/
getValueAsString(): string[];
getValueAsNumber(): number[];
setValue(value: string | number | Int64): void;
findValueAsText(value: number | UInt64): Variant;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { assert } from "node-opcua-assert";
import { DataType, Variant, VariantArrayType } from "node-opcua-variant";
import { coerceInt32, coerceInt64toInt32, coerceUInt64, Int32, Int64, isValidUInt64 } from "node-opcua-basic-types";
import { coerceInt32, coerceInt64toInt32, coerceUInt64, coerceUInt64toInt32, Int32, Int64, isValidInt64, isValidUInt64 } from "node-opcua-basic-types";
import { coerceLocalizedText, LocalizedText, QualifiedNameLike } from "node-opcua-data-model";
import { DataValue, DataValueT } from "node-opcua-data-value";
import { StatusCodes } from "node-opcua-status-code";
Expand All @@ -12,6 +12,7 @@ import { NumericRange } from "node-opcua-numeric-range";
import { DTEnumValue } from "node-opcua-nodeset-ua";
import { BindVariableOptions, INamespace, UAVariable, UAProperty, ISessionContext } from "node-opcua-address-space-base";
import { VariableTypeIds } from "node-opcua-constants";
import { _getBasicDataType, _getBasicDataTypeFromDataTypeNodeId } from "../get_basic_datatype";

import { registerNodePromoter } from "../../source/loader/register_node_promoter";
import { coerceEnumValues } from "../../source/helpers/coerce_enum_value";
Expand All @@ -21,17 +22,57 @@ import { UAVariableImpl } from "../ua_variable_impl";

import { add_dataItem_stuff } from "./add_dataItem_stuff";

function install_synchronization<T, DT extends DataType>(variable: UAMultiStateValueDiscreteEx<T, DT>) {
function convertToArray<T>(array: any): T[] {
if (Array.isArray(array)) return array;
const result: T[] = [];
for (let i = 0; i < array.length; i++) {
result[i] = array[i];
}
return result;
}
const getCoerceToInt32 = (dataType: DataType) => {
switch (dataType) {
case DataType.UInt64:
return coerceUInt64toInt32;
case DataType.Int64:
return coerceInt64toInt32;
default:
return coerceInt32;
}
};

function install_synchronization<T extends number | Int64 | Int64, DT extends DataType>(
variable: UAMultiStateValueDiscreteEx<T, DT>
) {
const _variable = variable as UAMultiStateValueDiscreteEx<T, DT>;
_variable.on("value_changed", (value: DataValue) => {
_variable.on("value_changed", (dataValue: DataValue) => {
const valueAsTextNode = variable.valueAsText || (_variable.getComponentByName("ValueAsText") as UAVariable);
if (!valueAsTextNode) {
return;
}
const valueAsText1 = _variable.findValueAsText(value.value.value);
valueAsTextNode.setValueFromSource(valueAsText1);
if (dataValue.value.arrayType === VariantArrayType.Array || dataValue.value.arrayType === VariantArrayType.Matrix) {
//
const coerce = getCoerceToInt32(_variable.getBasicDataType());

const values: number[] = convertToArray<Int32>(dataValue.value.value).map((a) => coerceInt32(a));
const variantArray: Variant[] = values.map((a) => _variable.findValueAsText(a));
const localizedText = variantArray.map((a) => a.value);

const valueAsText1 = new Variant({
arrayType: dataValue.value.arrayType,
dataType: DataType.LocalizedText,
value: localizedText,
dimensions: dataValue.value.dimensions
});
valueAsTextNode.setValueFromSource(valueAsText1);
} else {
const valueAsText1 = _variable.findValueAsText(dataValue.value.value);
valueAsTextNode.setValueFromSource(valueAsText1);
}
});
_variable.emit("value_changed", _variable.readValue());
const dataValue = _variable.readValue();
// detect value changes to update valueAsText (initial state)
_variable.emit("value_changed", dataValue);
}

export interface UAMultiStateValueDiscreteImpl<T, DT extends DataType> {
Expand All @@ -46,7 +87,10 @@ export interface UAMultiStateValueDiscreteImpl<T, DT extends DataType> {

readValueAsync(context: ISessionContext | null, callback?: any): any;
}
export class UAMultiStateValueDiscreteImpl<T, DT extends DataType> extends UAVariableImpl implements UAMultiStateValueDiscreteEx<T, DT> {
export class UAMultiStateValueDiscreteImpl<T, DT extends DataType>
extends UAVariableImpl
implements UAMultiStateValueDiscreteEx<T, DT>
{
public setValue(value: string | number | Int64): void {
if (typeof value === "string") {
const enumValues = this.enumValues.readValue().value.value;
Expand All @@ -61,12 +105,16 @@ export class UAMultiStateValueDiscreteImpl<T, DT extends DataType> extends UAVar
}
}

public getValueAsString(): string {
return this.valueAsText.readValue().value.value.text || "";
public getValueAsString(): any {
const v = this.valueAsText.readValue().value.value;
if (Array.isArray(v)) {
return v.map((a) => a.text);
}
return v.text || "";
}

public getValueAsNumber(): number {
return this.readValue().value.value as unknown as number;
public getValueAsNumber(): any {
return this.readValue().value.value;
}

public checkVariantCompatibility(value: Variant): StatusCode {
Expand All @@ -89,7 +137,7 @@ export class UAMultiStateValueDiscreteImpl<T, DT extends DataType> extends UAVar
public _isValueInRange(value: number): boolean {
// MultiStateValueDiscreteType
const enumValues = this.enumValues.readValue().value.value as DTEnumValue[];
const e = enumValues.findIndex((x: DTEnumValue) => coerceInt32(x.value) === value);
const e = enumValues.findIndex((x: DTEnumValue) => coerceInt64toInt32(x.value) === value);
return !(e === -1);
}
/**
Expand All @@ -115,17 +163,17 @@ export class UAMultiStateValueDiscreteImpl<T, DT extends DataType> extends UAVar
* @private
*/
public _setValue(value: Int64): void {
const int32Value = coerceInt64toInt32(value);
// check that value is in bound
if (!this._isValueInRange(coerceInt32(value))) {
if (!this._isValueInRange(int32Value)) {
throw new Error("UAMultiStateValueDiscrete#_setValue out of range " + value);
}

const dataType = this._getDataType();
if (dataType === DataType.Int64 || dataType === DataType.UInt64) {
this.setValueFromSource({ dataType, value });
this.setValueFromSource({ dataType, arrayType: VariantArrayType.Scalar, value });
} else {
const valueN = value[1];
this.setValueFromSource({ dataType, value: valueN });
this.setValueFromSource({ dataType, arrayType: VariantArrayType.Scalar, value: int32Value });
}
}

Expand All @@ -135,24 +183,22 @@ export class UAMultiStateValueDiscreteImpl<T, DT extends DataType> extends UAVar
*/
public findValueAsText(value?: number | Int64): Variant {
const enumValueIndex = this._enumValueIndex();

if (value === undefined) {
throw new Error("Unexpected undefined value");
}
if (value instanceof Array) {
value = value[1];
}
assert(!((value as any) instanceof Variant));
const valueAsInt32 = coerceInt64toInt32(value);

let valueAsText1 = "Invalid";
if (enumValueIndex[value]) {
valueAsText1 = enumValueIndex[value].displayName.text || `Invalid:${value}`;
if (enumValueIndex[valueAsInt32] !== undefined) {
valueAsText1 = enumValueIndex[valueAsInt32].displayName.text || `Invalid:${value}`;
}
const result = new Variant({
dataType: DataType.LocalizedText,
value: coerceLocalizedText(valueAsText1)
});
return result;
}

public _getDataType(): DataType {
if (this.dataType.value === 26 /* Number */) {
return DataType.UInt32;
Expand All @@ -170,7 +216,7 @@ export class UAMultiStateValueDiscreteImpl<T, DT extends DataType> extends UAVar
// this includes signed and unsigned integers from 8 to 64 Bit length.

// istanbul ignore next
validateDataType(this.dataType.value);
validateIsNumericDataType(this.dataType.value);

// find the enum value type
install_synchronization(this);
Expand Down Expand Up @@ -212,30 +258,33 @@ export function _addMultiStateValueDiscrete<T, DT extends DataType>(
if (options.value === undefined && enumValues[0]) {
options.value = enumValues[0].value; // Int64
}
// Only DataTypes that can be represented with EnumValues are allowed for Variables of MultiStateValueDiscreteType.
// These are Integers up to 64 Bits (signed and unsigned).:

const dataType: DataType = _getBasicDataTypeFromDataTypeNodeId(addressSpace, options.dataType || DataType.UInt32);

let value: undefined | BindVariableOptions;
if (typeof options.value === "number" || isValidUInt64(options.value as number | number[])) {
if (isValidUInt64(options.value as number | number[])) {
value = new Variant({
dataType: DataType.UInt32,
value: (options.value as Int64)[1] // Low word
});
} else {
value = new Variant({
dataType: DataType.UInt32,
value: options.value
});
}
if (
typeof options.value === "number" ||
isValidUInt64(options.value as number | number[]) ||
isValidInt64(options.value as number | number[])
) {
value = new Variant({
dataType,
value: options.value
});
} else {
value = options.value as any;
}

const cloned_options = {
...options,
dataType: DataType.UInt32,
dataType,
typeDefinition: multiStateValueDiscreteType.nodeId,
// valueRank:
// note : OPCUA Spec 1.03 specifies -1:Scalar (part 8 page 8) but nodeset file specifies -2:Any
value,
// limitation: although the Specs specify -2:any, we only support -1(Scalar)
valueRank: -1 // -1 : Scalar
};

Expand Down Expand Up @@ -267,6 +316,8 @@ export function _addMultiStateValueDiscrete<T, DT extends DataType>(
minimumSamplingInterval: 0,
modellingRule: options.modellingRule ? "Mandatory" : undefined,
propertyOf: variable,
arrayDimensions: options.arrayDimensions,
valueRank: options.valueRank,
typeDefinition: "PropertyType",
userAccessLevel: "CurrentRead"
// value: valueAsText
Expand All @@ -282,21 +333,23 @@ export function _addMultiStateValueDiscrete<T, DT extends DataType>(
return variable;
}

export function validateDataType(dataTypeValue: any): void {
const validTypes = [
DataType.UInt64,
DataType.Int64,
DataType.UInt32,
DataType.Int32,
DataType.UInt16,
DataType.Int16,
DataType.Byte,
DataType.Byte,
DataType.SByte,
26 /*Number*/
];

if (typeof dataTypeValue !== "number" || validTypes.indexOf(dataTypeValue) < 0) {
const validBasicNumericDataTypes = [
DataType.UInt64,
DataType.Int64,
DataType.UInt32,
DataType.Int32,
DataType.UInt16,
DataType.Int16,
DataType.Byte,
DataType.Byte,
DataType.SByte,
26 /*Number*/
];
export function validateIsNumericDataType(dataTypeValue: any): void {
if (typeof dataTypeValue !== "number" || validBasicNumericDataTypes.indexOf(dataTypeValue) < 0) {
throw new Error(`Invalid DataType in UAMultiStateValueDiscrete => ${dataTypeValue.toString()}`);
}
}

/** @deprecated: use validateIsNumericDataType instead */
export const validateDataType = validateIsNumericDataType;
23 changes: 16 additions & 7 deletions packages/node-opcua-address-space/src/get_basic_datatype.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IAddressSpace } from "node-opcua-address-space-base";
import { DataType } from "node-opcua-basic-types";
import { NodeClass } from "node-opcua-data-model";
import { NodeId } from "node-opcua-nodeid";
import { NodeId, NodeIdLike } from "node-opcua-nodeid";

export interface IBaseNodeVariableOrVariableType {
addressSpace: IAddressSpace;
Expand All @@ -10,6 +10,19 @@ export interface IBaseNodeVariableOrVariableType {
interface IBaseNodeVariableOrVariableTypeEx extends IBaseNodeVariableOrVariableType {
_basicDataType: DataType;
}

export function _getBasicDataTypeFromDataTypeNodeId(
addressSpace: IAddressSpace,
dataTypeNodeId: { nodeId: NodeIdLike } | NodeIdLike
): DataType {
const dataTypeNodeId_ = (dataTypeNodeId as any).nodeId ? (dataTypeNodeId as any).nodeId : dataTypeNodeId;
const dataTypeNode = addressSpace.findDataType(dataTypeNodeId_);
if (!dataTypeNode) {
return DataType.Null;
}
return dataTypeNode.getBasicDataType();
}

export function _getBasicDataType(uaNode: IBaseNodeVariableOrVariableType): DataType {
const _uaNode = uaNode as IBaseNodeVariableOrVariableTypeEx;
if (_uaNode._basicDataType) {
Expand All @@ -23,10 +36,6 @@ export function _getBasicDataType(uaNode: IBaseNodeVariableOrVariableType): Data
// may be node has been deleted already
return DataType.Null;
}
const dataTypeNode = addressSpace.findDataType(_uaNode.dataType)!;
const basicDataType =
dataTypeNode && dataTypeNode.nodeClass === NodeClass.DataType ? dataTypeNode.getBasicDataType() : DataType.Null;
// const basicDataType = addressSpace.findCorrespondingBasicDataType(uaNode.dataType);
_uaNode._basicDataType = basicDataType;
return basicDataType;
_uaNode._basicDataType = _getBasicDataTypeFromDataTypeNodeId(addressSpace, _uaNode.dataType);
return _uaNode._basicDataType;
}

0 comments on commit 77f1bf6

Please sign in to comment.