diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts index a781c52c71..93baf124a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts @@ -15,15 +15,17 @@ /// import { GatewayConnector, GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models'; -import { isNumber, isString } from '@core/utils'; +import { + GatewayConnectorVersionMappingUtil +} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util'; export abstract class GatewayConnectorVersionProcessor { gatewayVersion: number; configVersion: number; protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector) { - this.gatewayVersion = this.parseVersion(this.gatewayVersionIn); - this.configVersion = this.parseVersion(this.connector.configVersion); + this.gatewayVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.gatewayVersionIn); + this.configVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.connector.configVersion); } getProcessedByVersion(): GatewayConnector { @@ -53,19 +55,13 @@ export abstract class GatewayConnectorVersionProcessor { } private isVersionUpgradeNeeded(): boolean { - return this.gatewayVersionIn === GatewayVersion.Current && (!this.configVersion || this.configVersion < this.gatewayVersion); + return this.gatewayVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current) + && (!this.configVersion || this.configVersion < this.gatewayVersion); } private isVersionDowngradeNeeded(): boolean { - return this.configVersion && this.connector.configVersion === GatewayVersion.Current && (this.configVersion > this.gatewayVersion); - } - - private parseVersion(version: string | number): number { - if (isNumber(version)) { - return version as number; - } - - return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0; + return this.configVersion && this.configVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current) + && (this.configVersion > this.gatewayVersion); } protected abstract getDowngradedVersion(): GatewayConnector; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 134a9baf0a..7008f3c70a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -175,7 +175,7 @@
- {{ 'gateway.send-data-TB' | translate }} + {{ 'gateway.send-data-to-platform' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts index 18d1a96e38..a6ee3ce93d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts @@ -126,6 +126,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On if (!securityInfo.type) { securityInfo.type = SecurityType.ANONYMOUS; } + this.updateValidators(securityInfo.type); this.securityFormGroup.reset(securityInfo, {emitEvent: false}); } this.cdr.markForCheck(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index c8235cb697..1c0f74e8e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -37,6 +37,7 @@ import { Observable, Subject } from 'rxjs'; import { ResourcesService } from '@core/services/resources.service'; import { takeUntil, tap } from 'rxjs/operators'; import { helpBaseUrl } from '@shared/models/constants'; +import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe'; @Component({ selector: 'tb-add-connector-dialog', @@ -63,6 +64,7 @@ export class AddConnectorDialogComponent @Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData, public dialogRef: MatDialogRef, private fb: FormBuilder, + private isLatestVersionConfig: LatestVersionConfigPipe, private resourcesService: ResourcesService) { super(store, router, dialogRef); this.connectorForm = this.fb.group({ @@ -103,9 +105,9 @@ export class AddConnectorDialogComponent if (gatewayVersion) { value.configVersion = gatewayVersion; } - value.configurationJson = (gatewayVersion === GatewayVersion.Current - ? defaultConfig[this.data.gatewayVersion] - : defaultConfig.legacy) + value.configurationJson = (this.isLatestVersionConfig.transform(gatewayVersion) + ? defaultConfig[GatewayVersion.Current] + : defaultConfig[GatewayVersion.Legacy]) ?? defaultConfig; if (this.connectorForm.valid) { this.dialogRef.close(value); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index cad7af4392..a700dcc94b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -178,7 +178,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 8ca92d4ca7..00fd3e19c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -59,7 +59,6 @@ import { GatewayConnectorDefaultTypesTranslatesMap, GatewayLogLevel, noLeadTrailSpacesRegex, - GatewayVersion, ReportStrategyDefaultValue, ReportStrategyType, } from './gateway-widget.models'; @@ -71,6 +70,7 @@ import { PageData } from '@shared/models/page/page-data'; import { GatewayConnectorVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util'; +import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe'; export class ForceErrorStateMatcher implements ErrorStateMatcher { isErrorState(control: FormControl | null): boolean { @@ -104,7 +104,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; readonly ConnectorConfigurationModes = ConfigurationModes; - readonly GatewayVersion = GatewayVersion; readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue; pageLink: PageLink; @@ -149,6 +148,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private telemetryWsService: TelemetryWebsocketService, private zone: NgZone, private utils: UtilsService, + private isLatestVersionConfig: LatestVersionConfigPipe, private cd: ChangeDetectorRef) { super(store); @@ -255,7 +255,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie delete value.class; } - if (value.type === ConnectorType.MODBUS && value.configVersion === GatewayVersion.Current) { + if (value.type === ConnectorType.MODBUS && this.isLatestVersionConfig.transform(value.configVersion)) { if (!value.reportStrategy) { value.reportStrategy = { type: ReportStrategyType.OnReportPeriod, @@ -508,6 +508,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie if (!connector.configurationJson) { connector.configurationJson = {} as ConnectorBaseConfig; } + if (this.gatewayVersion && !connector.configVersion) { + connector.configVersion = this.gatewayVersion; + } connector.basicConfig = connector.configurationJson; this.initialConnector = connector; @@ -517,7 +520,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.saveConnector(this.getUpdatedConnectorData(connector)); - if (!previousType || previousType === connector.type || !this.allowBasicConfig.has(connector.type)) { + if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) { this.patchBasicConfigConnector(connector); } else { this.basicConfigInitSubject.pipe(take(1)).subscribe(() => { @@ -527,24 +530,26 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } private setInitialConnectorValues(connector: GatewayConnector): void { + const {basicConfig, mode, ...initialConnector} = connector; this.toggleReportStrategy(connector.type); this.connectorForm.get('mode').setValue(this.allowBasicConfig.has(connector.type) ? connector.mode ?? ConfigurationModes.BASIC : null, {emitEvent: false} ); - this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false}); - this.connectorForm.get('type').setValue(connector.type, {emitEvent: false}); + this.connectorForm.patchValue(initialConnector, {emitEvent: false}); } private openAddConnectorDialog(): Observable { - return this.dialog.open(AddConnectorDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - dataSourceData: this.dataSource.data, - gatewayVersion: this.gatewayVersion, - } - }).afterClosed(); + return this.ctx.ngZone.run(() => + this.dialog.open(AddConnectorDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataSourceData: this.dataSource.data, + gatewayVersion: this.gatewayVersion, + } + }).afterClosed() + ); } uniqNameRequired(): ValidatorFn { @@ -650,13 +655,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private observeModeChange(): void { this.connectorForm.get('mode').valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe((mode) => { + .subscribe(() => { this.connectorForm.get('mode').markAsPristine(); - if (mode === ConfigurationModes.BASIC) { - this.basicConfigInitSubject.pipe(take(1)).subscribe(() => { - this.patchBasicConfigConnector({...this.initialConnector, mode: ConfigurationModes.BASIC}); - }); - } }); } @@ -827,12 +827,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie ...connector, }, this.gatewayVersion); + if (this.gatewayVersion && !connectorState.configVersion) { + connectorState.configVersion = this.gatewayVersion; + } + connectorState.basicConfig = connectorState.configurationJson; this.initialConnector = connectorState; this.updateConnector(connectorState); } private updateConnector(connector: GatewayConnector): void { + this.jsonConfigSub?.unsubscribe(); switch (connector.type) { case ConnectorType.MQTT: case ConnectorType.OPCUA: @@ -842,18 +847,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie default: this.connectorForm.patchValue({...connector, mode: null}); this.connectorForm.markAsPristine(); + this.createJsonConfigWatcher(); } - this.createJsonConfigWatcher(); } private updateBasicConfigConnector(connector: GatewayConnector): void { + this.basicConfigSub?.unsubscribe(); + const previousType = this.connectorForm.get('type').value; this.setInitialConnectorValues(connector); - if ((!connector.mode || connector.mode === ConfigurationModes.BASIC) && this.connectorForm.get('type').value !== connector.type) { + + if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) { + this.patchBasicConfigConnector(connector); + } else { this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => { this.patchBasicConfigConnector(connector); }); - } else { - this.patchBasicConfigConnector(connector); } } @@ -861,6 +869,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.connectorForm.patchValue(connector, {emitEvent: false}); this.connectorForm.markAsPristine(); this.createBasicConfigWatcher(); + this.createJsonConfigWatcher(); } private toggleReportStrategy(type: ConnectorType): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts index 06526ce08e..27b5285166 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts @@ -33,6 +33,8 @@ export class GatewayHelpLinkPipe implements PipeTransform { } else { return; } + } else if (field === 'attributes' || field === 'timeseries') { + return 'widget/lib/gateway/attributes_timeseries_expressions_fn'; } return 'widget/lib/gateway/expressions_fn'; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts new file mode 100644 index 0000000000..56484dde35 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/latest-version-config.pipe.ts @@ -0,0 +1,32 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; +import { GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { + GatewayConnectorVersionMappingUtil +} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util'; + +@Pipe({ + name: 'isLatestVersionConfig', + standalone: true, +}) +export class LatestVersionConfigPipe implements PipeTransform { + transform(configVersion: number | string): boolean { + return GatewayConnectorVersionMappingUtil.parseVersion(configVersion) + >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts index 90c89ea316..fe507c1c46 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts @@ -24,6 +24,7 @@ import { import { MqttVersionProcessor } from '@home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract'; import { OpcVersionProcessor } from '@home/components/widget/lib/gateway/abstract/opc-version-processor.abstract'; import { ModbusVersionProcessor } from '@home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract'; +import { isNumber, isString } from '@core/utils'; export abstract class GatewayConnectorVersionMappingUtil { @@ -39,4 +40,12 @@ export abstract class GatewayConnectorVersionMappingUtil { return connector; } } + + static parseVersion(version: string | number): number { + if (isNumber(version)) { + return version as number; + } + + return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts index 9837f2f15d..f589dfea97 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts @@ -56,7 +56,7 @@ export class OpcVersionMappingUtil { static mapMappingToUpgradedVersion(mapping: LegacyDeviceConnectorMapping[]): DeviceConnectorMapping[] { return mapping.map((legacyMapping: LegacyDeviceConnectorMapping) => ({ ...legacyMapping, - deviceNodeSource: this.getTypeSourceByValue(legacyMapping.deviceNodePattern), + deviceNodeSource: this.getDeviceNodeSourceByValue(legacyMapping.deviceNodePattern), deviceInfo: { deviceNameExpression: legacyMapping.deviceNamePattern, deviceNameExpressionSource: this.getTypeSourceByValue(legacyMapping.deviceNamePattern), @@ -122,6 +122,14 @@ export class OpcVersionMappingUtil { return OPCUaSourceType.CONST; } + private static getDeviceNodeSourceByValue(value: string): OPCUaSourceType { + if (value.includes('${')) { + return OPCUaSourceType.IDENTIFIER; + } else { + return OPCUaSourceType.PATH; + } + } + private static getArgumentType(arg: unknown): string { switch (typeof arg) { case 'boolean': diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 9d08ee0324..1b044e787d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -167,9 +167,10 @@ import { ModbusRpcParametersComponent } from '@home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component'; import { RpcTemplateArrayViewPipe } from '@home/components/widget/lib/gateway/pipes/rpc-template-array-view.pipe'; -import { +import { ReportStrategyComponent } from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component'; +import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe'; @NgModule({ declarations: [ @@ -268,6 +269,7 @@ import { ModbusRpcParametersComponent, RpcTemplateArrayViewPipe, ReportStrategyComponent, + LatestVersionConfigPipe, ], exports: [ EntitiesTableWidgetComponent, @@ -337,7 +339,8 @@ import { ScadaSymbolWidgetComponent ], providers: [ - {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule} + {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}, + {provide: LatestVersionConfigPipe} ] }) export class WidgetComponentsModule { diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md new file mode 100644 index 0000000000..91cf23ce26 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md @@ -0,0 +1,65 @@ +### Expressions +#### JSON Path: + +The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages: + + - The JSONPath format can be used to extract data from the message body. + + - The regular expression format can be used to extract data from the topic where the message will arrive. + + - Slices can only be used in the expression fields of bytes converters. + +JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured: + +- `$`: The root element of the JSON document; + +- `.`: Child operator used to select child elements. For example, $.store.book ; + +- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object; + +##### Examples: + +For example, if we want to extract the device name from the following message, we can use the expression below: + +MQTT message: + +``` +{ + "sensorModelInfo": { + "sensorName": "AM-123", + "sensorType": "myDeviceType" + }, + "data": { + "temp": 12.2, + "hum": 56, + "status": "ok" + } +} +{:copy-code} +``` + +Expression: + +`${sensorModelInfo.sensorName}` + +Converted data: + +`AM-123` + +If we want to extract all data from the message above, we can use the following expression: + +`${data}` + +Converted data: + +`{"temp": 12.2, "hum": 56, "status": "ok"}` + +Or if we want to extract specific data (for example “temperature”), you can use the following expression: + +`${data.temp}` + +And as a converted data we will get: + +`12.2` + +
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 6f41308f43..d4a8cffa53 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3277,7 +3277,7 @@ }, "select-connector": "Select connector to display config", "send-change-data": "Send data only on change", - "send-data-TB": "Send data to ThingsBoard", + "send-data-to-platform": "Send data to platform", "send-data-on-change": "Send data only on change", "send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.", "server": "Server", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json index 019d2062f8..cfa983710b 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json @@ -20,13 +20,21 @@ "connectAttemptTimeMs": 5000, "connectAttemptCount": 5, "waitAfterFailedAttemptsMs": 300000, + "reportStrategy": { + "type": "ON_REPORT_PERIOD", + "reportPeriod": 30000 + }, "attributes": [ { "tag": "string_read", "type": "string", "functionCode": 4, "objectsCount": 4, - "address": 1 + "address": 1, + "reportStrategy": { + "type": "ON_REPORT_PERIOD", + "reportPeriod": 15000 + } }, { "tag": "bits_read", @@ -86,7 +94,11 @@ "type": "8uint", "functionCode": 4, "objectsCount": 1, - "address": 17 + "address": 17, + "reportStrategy": { + "type": "ON_REPORT_PERIOD", + "reportPeriod": 15000 + } }, { "tag": "16uint_read",