Browse Source

Merge pull request #11354 from maxunbearable/improvements/050824-gateway

Gateway Improvements
pull/11373/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
78257ee9f6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      ui-ngx/src/app/modules/common/modules-map.ts
  2. 13
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  3. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html
  4. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html
  5. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss
  6. 40
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts
  7. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  8. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss
  9. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  10. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  11. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts
  12. 92
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html
  13. 166
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts
  14. 19
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  15. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss
  16. 41
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  17. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  18. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss
  19. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  20. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss
  21. 39
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts
  22. 31
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts
  23. 77
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  24. 40
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html
  25. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  26. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  27. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  28. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  29. 67
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  30. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  31. 18
      ui-ngx/src/app/shared/directives/public-api.ts
  32. 2
      ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts
  33. 1
      ui-ngx/src/app/shared/public-api.ts
  34. 25
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6
ui-ngx/src/app/modules/common/modules-map.ts

@ -99,6 +99,9 @@ import * as TbJsonPipe from '@shared/pipe/tbJson.pipe';
import * as TruncatePipe from '@shared/pipe/truncate.pipe'; import * as TruncatePipe from '@shared/pipe/truncate.pipe';
import * as ImagePipe from '@shared/pipe/image.pipe'; import * as ImagePipe from '@shared/pipe/image.pipe';
import * as EllipsisChipListDirective from '@shared/directives/ellipsis-chip-list.directive';
import * as TruncateWithTooltipDirective from '@shared/directives/truncate-with-tooltip.directive';
import * as coercion from '@shared/decorators/coercion'; import * as coercion from '@shared/decorators/coercion';
import * as enumerable from '@shared/decorators/enumerable'; import * as enumerable from '@shared/decorators/enumerable';
import * as TbInject from '@shared/decorators/tb-inject'; import * as TbInject from '@shared/decorators/tb-inject';
@ -422,6 +425,9 @@ class ModulesMap implements IModulesMap {
'@shared/pipe/truncate.pipe': TruncatePipe, '@shared/pipe/truncate.pipe': TruncatePipe,
'@shared/pipe/image.pipe': ImagePipe, '@shared/pipe/image.pipe': ImagePipe,
'@shared/directives/ellipsis-chip-list.directive': EllipsisChipListDirective,
'@shared/directives/truncate-with-tooltip.directive': TruncateWithTooltipDirective,
'@shared/decorators/coercion': coercion, '@shared/decorators/coercion': coercion,
'@shared/decorators/enumerable': enumerable, '@shared/decorators/enumerable': enumerable,
'@shared/decorators/tb-inject': TbInject, '@shared/decorators/tb-inject': TbInject,

13
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts

@ -109,12 +109,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
} }
writeValue(brokerConfig: BrokerConfig): void { writeValue(brokerConfig: BrokerConfig): void {
const brokerConfigState = { const {
...brokerConfig, version = 5,
version: brokerConfig.version || 5, clientId = `tb_gw_${generateSecret(5)}`,
clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5), security = {},
}; } = brokerConfig;
this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false});
this.brokerConfigFormGroup.reset({ ...brokerConfig, version, clientId, security }, { emitEvent: false });
this.cdr.markForCheck(); this.cdr.markForCheck();
} }

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html

@ -18,7 +18,7 @@
<div class="tb-mapping-table tb-absolute-fill"> <div class="tb-mapping-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-mapping-table-content"> <div fxFlex fxLayout="column" class="tb-mapping-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools" *ngIf="(dataSource.isEmpty() | async) === false">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span> <span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span>
</div> </div>
@ -75,15 +75,22 @@
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index" <mat-cell *matCellDef="let mapping; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> [ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <ng-template #rowActions>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageMapping($event, i)"> (click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon> <tb-icon>edit</tb-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteMapping($event, i)"> (click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon> <tb-icon>delete</tb-icon>
</button> </button>
</ng-template>
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div> </div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> <div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button <button mat-icon-button
@ -92,14 +99,7 @@
<mat-icon class="material-icons">more_vert</mat-icon> <mat-icon class="material-icons">more_vert</mat-icon>
</button> </button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button <ng-container [ngTemplateOutlet]="rowActions"></ng-container>
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu> </mat-menu>
</div> </div>
</mat-cell> </mat-cell>

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html

@ -23,6 +23,16 @@
<tb-modbus-master-table formControlName="master"></tb-modbus-master-table> <tb-modbus-master-table formControlName="master"></tb-modbus-master-table>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.server-config' | translate }}"> <mat-tab label="{{ 'gateway.server-config' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" [formControl]="enableSlaveControl">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<tb-modbus-slave-config formControlName="slave"></tb-modbus-slave-config> <tb-modbus-slave-config formControlName="slave"></tb-modbus-slave-config>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss

@ -16,9 +16,3 @@
:host { :host {
height: 100%; height: 100%;
} }
:host ::ng-deep {
.mat-mdc-tab-body-content {
overflow: hidden !important;
}
}

40
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts

@ -18,13 +18,15 @@ import { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, Templ
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
FormControl,
FormGroup, FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormControl,
ValidationErrors, ValidationErrors,
Validator, Validator,
} from '@angular/forms'; } from '@angular/forms';
import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -33,6 +35,7 @@ import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
import { isEqual } from '@core/utils';
@Component({ @Component({
selector: 'tb-modbus-basic-config', selector: 'tb-modbus-basic-config',
@ -66,6 +69,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
@Input() generalTabContent: TemplateRef<any>; @Input() generalTabContent: TemplateRef<any>;
basicFormGroup: FormGroup; basicFormGroup: FormGroup;
enableSlaveControl: FormControl<boolean>;
onChange: (value: ModbusBasicConfig) => void; onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void; onTouched: () => void;
@ -77,13 +81,21 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
master: [], master: [],
slave: [], slave: [],
}); });
this.enableSlaveControl = new FormControl(false);
this.basicFormGroup.valueChanges this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(value => { .subscribe(({ master, slave }) => {
this.onChange(value); this.onChange({ master, slave: slave ?? {} });
this.onTouched(); this.onTouched();
}); });
this.enableSlaveControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(enable => {
this.updateSlaveEnabling(enable);
this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange});
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -106,11 +118,25 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
}; };
this.basicFormGroup.setValue(editedBase, {emitEvent: false}); this.basicFormGroup.setValue(editedBase, {emitEvent: false});
this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {}));
} }
validate(): ValidationErrors | null { validate(basicFormControl: UntypedFormControl): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { const { master, slave } = basicFormControl.value;
basicFormGroup: {valid: false} const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave);
}; if (!this.basicFormGroup.valid || isEmpty) {
return {
basicFormGroup: {valid: false}
};
}
return null;
}
private updateSlaveEnabling(isEnabled: boolean): void {
if (isEnabled) {
this.basicFormGroup.get('slave').enable({emitEvent: false});
} else {
this.basicFormGroup.get('slave').disable({emitEvent: false});
}
} }
} }

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -26,12 +26,16 @@
<mat-expansion-panel class="tb-settings" [expanded]="last"> <mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<div class="title-container"> <div *ngIf="isMaster else tagName" class="title-container" tbTruncateWithTooltip>
<span *ngIf="isMaster else tagName"> {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</span>
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template>
</div> </div>
<ng-template #tagName>
<div class="tb-flex">
<div class="title-container" tbTruncateWithTooltip>{{ 'gateway.key' | translate }}: {{ keyControl.get('tag').value }}</div>
<div class="title-container">{{ 'gateway.address' | translate }}: {{ keyControl.get('address').value }}</div>
<div class="title-container">{{ 'gateway.type' | translate }}: {{ keyControl.get('type').value }}</div>
</div>
</ng-template>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
@ -98,7 +102,7 @@
name="value" name="value"
formControlName="objectsCount" formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}" placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!editableDataTypes.includes(keyControl.get('type').value)" [readonly]="!ModbusEditableDataTypes.includes(keyControl.get('type').value)"
/> />
</mat-form-field> </mat-form-field>
</div> </div>

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss

@ -20,10 +20,7 @@
max-width: 700px; max-width: 700px;
.title-container { .title-container {
max-width: 11vw; width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
} }
.key-panel { .key-panel {

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -27,6 +27,7 @@ import {
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { import {
ModbusDataType, ModbusDataType,
ModbusEditableDataTypes,
ModbusFunctionCodeTranslationsMap, ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType, ModbusObjectCountByDataType,
ModbusValue, ModbusValue,
@ -40,6 +41,7 @@ import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-data-keys-panel', selector: 'tb-modbus-data-keys-panel',
@ -50,6 +52,7 @@ import { Subject } from 'rxjs';
CommonModule, CommonModule,
SharedModule, SharedModule,
GatewayHelpLinkPipe, GatewayHelpLinkPipe,
TruncateWithTooltipDirective,
] ]
}) })
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@ -72,14 +75,15 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
functionCodesMap = new Map(); functionCodesMap = new Map();
defaultFunctionCodes = []; defaultFunctionCodes = [];
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4]; private readonly defaultReadFunctionCodes = [3, 4];
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; private readonly bitsReadFunctionCodes = [1, 2];
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16]; private readonly defaultWriteFunctionCodes = [6, 16];
private readonly bitsWriteFunctionCodes = [5, 15];
constructor(private fb: UntypedFormBuilder) {} constructor(private fb: UntypedFormBuilder) {}
@ -161,7 +165,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
private observeKeyDataType(keyFormGroup: FormGroup): void { private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.editableDataTypes.includes(dataType)) { if (!this.ModbusEditableDataTypes.includes(dataType)) {
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
} }
this.updateFunctionCodes(keyFormGroup, dataType); this.updateFunctionCodes(keyFormGroup, dataType);
@ -177,23 +181,23 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
} }
private getFunctionCodes(dataType: ModbusDataType): number[] { private getFunctionCodes(dataType: ModbusDataType): number[] {
const writeFunctionCodes = [
...(dataType === ModbusDataType.BITS ? this.bitsWriteFunctionCodes : []), ...this.defaultWriteFunctionCodes
];
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return dataType === ModbusDataType.STRING return writeFunctionCodes.sort((a, b) => a - b);
? this.stringAttrUpdatesWriteFunctionCodes
: this.defaultWriteFunctionCodes;
} }
const functionCodes = [...this.defaultReadFunctionCodes]; const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) { if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2]; functionCodes.push(...this.bitsReadFunctionCodes);
functionCodes.push(...bitsFunctionCodes);
functionCodes.sort();
} }
if (this.keysType === ModbusValueKey.RPC_REQUESTS) { if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
functionCodes.push(...this.defaultWriteFunctionCodes); functionCodes.push(...writeFunctionCodes);
} }
return functionCodes; return functionCodes.sort((a, b) => a - b);
} }
private getDefaultFunctionCodes(): number[] { private getDefaultFunctionCodes(): number[] {

37
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html

@ -16,9 +16,12 @@
--> -->
<div class="tb-master-table tb-absolute-fill"> <div class="tb-master-table tb-absolute-fill">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-master' | translate }}</div>
</div>
<div fxFlex fxLayout="column" class="tb-master-table-content"> <div fxFlex fxLayout="column" class="tb-master-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools" *ngIf="(dataSource.isEmpty() | async) === false">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span> <span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span>
</div> </div>
@ -63,33 +66,40 @@
<mat-header-cell *matHeaderCellDef class="table-value-column"> <mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }} {{ 'gateway.name' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column"> <mat-cell *matCellDef="let slave" class="table-value-column">
{{ mapping['name'] }} {{ slave['name'] }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="'type'"> <ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column"> <mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }} {{ 'gateway.client-communication-type' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column"> <mat-cell *matCellDef="let slave" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(mapping['type']) }} {{ ModbusProtocolLabelsMap.get(slave['type']) }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef <mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}"> [ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}">
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index" <mat-cell *matCellDef="let slave; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> [ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <ng-template #rowActions>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageSlave($event, i)"> (click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon> <tb-icon>edit</tb-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
(click)="deleteMapping($event, i)"> matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteSlave($event, i)">
<tb-icon>delete</tb-icon> <tb-icon>delete</tb-icon>
</button> </button>
</ng-template>
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div> </div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> <div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button <button mat-icon-button
@ -98,20 +108,13 @@
<mat-icon class="material-icons">more_vert</mat-icon> <mat-icon class="material-icons">more_vert</mat-icon>
</button> </button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button <ng-container [ngTemplateOutlet]="rowActions"></ng-container>
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu> </mat-menu>
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row> <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions']"></mat-row> <mat-row *matRowDef="let slave; columns: ['name', 'type', 'actions']"></mat-row>
</table> </table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center" <section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new"> class="mat-headline-5 tb-absolute-fill tb-add-new">

26
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts

@ -34,18 +34,17 @@ import {
ControlValueAccessor, ControlValueAccessor,
FormArray, FormArray,
FormBuilder, FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormGroup, UntypedFormGroup,
ValidationErrors,
Validator,
} from '@angular/forms'; } from '@angular/forms';
import { import {
ModbusMasterConfig, ModbusMasterConfig,
ModbusProtocolLabelsMap, ModbusProtocolLabelsMap,
ModbusSlaveInfo,
ModbusValues,
SlaveConfig SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component';
@ -62,16 +61,11 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs
useExisting: forwardRef(() => ModbusMasterTableComponent), useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true multi: true
}, },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
}
], ],
standalone: true, standalone: true,
imports: [CommonModule, SharedModule] imports: [CommonModule, SharedModule]
}) })
export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
@ViewChild('searchInput') searchInputField: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef;
@ -138,12 +132,6 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
this.pushDataAsFormArrays(master.slaves); this.pushDataAsFormArrays(master.slaves);
} }
validate(): ValidationErrors | null {
return this.slaves.controls.length ? null : {
slavesFormGroup: {valid: false}
};
}
enterFilterMode(): void { enterFilterMode(): void {
this.textSearchMode = true; this.textSearchMode = true;
this.cdr.detectChanges(); this.cdr.detectChanges();
@ -164,12 +152,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
} }
const withIndex = isDefinedAndNotNull(index); const withIndex = isDefinedAndNotNull(index);
const value = withIndex ? this.slaves.at(index).value : {}; const value = withIndex ? this.slaves.at(index).value : {};
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, { this.dialog.open<ModbusSlaveDialogComponent, ModbusSlaveInfo, ModbusValues>(ModbusSlaveDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
value, value,
buttonTitle: withIndex ? 'action.add' : 'action.apply' buttonTitle: withIndex ? 'action.apply' : 'action.add'
} }
}).afterClosed() }).afterClosed()
.pipe(take(1), takeUntil(this.destroy$)) .pipe(take(1), takeUntil(this.destroy$))
@ -185,7 +173,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
}); });
} }
deleteMapping($event: Event, index: number): void { deleteSlave($event: Event, index: number): void {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }

92
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html

@ -0,0 +1,92 @@
<!--
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.
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<div fxFlex fxLayout="row">
<mat-form-field fxFlex="100">
<mat-label>{{ 'gateway.key' | translate }}</mat-label>
<input matInput name="value" formControlName="tag" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="rpcParametersFormGroup.get('tag').hasError('required') &&
rpcParametersFormGroup.get('tag').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusDataTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.functionCode' | translate }}</mat-label>
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of functionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex fxLayout="row">
<mat-form-field fxFlex="100" *ngIf="writeFunctionCodes.includes(rpcParametersFormGroup.get('functionCode').value)">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<input matInput name="value" formControlName="value" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="rpcParametersFormGroup.get('value').hasError('required') &&
rpcParametersFormGroup.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.address' | translate }}</mat-label>
<input matInput type="number" min="0" max="50000" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.address-required') | translate"
*ngIf="rpcParametersFormGroup.get('address').hasError('required') &&
rpcParametersFormGroup.get('address').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.objectsCount' | translate }}</mat-label>
<input
matInput
type="number"
min="1"
max="50000"
name="value"
formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!ModbusEditableDataTypes.includes(rpcParametersFormGroup.get('type').value)"
/>
</mat-form-field>
</div>
</ng-container>

166
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts

@ -0,0 +1,166 @@
///
/// 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 {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
ModbusDataType,
ModbusEditableDataTypes,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
noLeadTrailSpacesRegex,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-modbus-rpc-parameters',
templateUrl: './modbus-rpc-parameters.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusRpcParametersComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusRpcParametersComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class ModbusRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy {
rpcParametersFormGroup: UntypedFormGroup;
functionCodes: Array<number>;
readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly modbusDataTypes = Object.values(ModbusDataType) as ModbusDataType[];
readonly writeFunctionCodes = [5, 6, 15, 16];
private readonly defaultFunctionCodes = [3, 4, 6, 16];
private readonly readFunctionCodes = [1, 2, 3, 4];
private readonly bitsFunctionCodes = [...this.readFunctionCodes, ...this.writeFunctionCodes];
private onChange: (value: ModbusValue) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.rpcParametersFormGroup = this.fb.group({
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusDataType.BYTES, [Validators.required]],
functionCode: [this.defaultFunctionCodes[0], [Validators.required]],
value: [{value: '', disabled: true}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
address: [null, [Validators.required]],
objectsCount: [1, [Validators.required]],
});
this.updateFunctionCodes(this.rpcParametersFormGroup.get('type').value);
this.observeValueChanges();
this.observeKeyDataType();
this.observeFunctionCode();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusValue) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.rpcParametersFormGroup.valid ? null : {
rpcParametersFormGroup: { valid: false }
};
}
writeValue(value: ModbusValue): void {
this.rpcParametersFormGroup.patchValue(value, {emitEvent: false});
}
private observeValueChanges(): void {
this.rpcParametersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
private observeKeyDataType(): void {
this.rpcParametersFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.ModbusEditableDataTypes.includes(dataType)) {
this.rpcParametersFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
this.updateFunctionCodes(dataType);
});
}
private observeFunctionCode(): void {
this.rpcParametersFormGroup.get('functionCode').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(code => this.updateValueEnabling(code));
}
private updateValueEnabling(code: number): void {
if (this.writeFunctionCodes.includes(code)) {
this.rpcParametersFormGroup.get('value').enable({emitEvent: false});
} else {
this.rpcParametersFormGroup.get('value').setValue(null);
this.rpcParametersFormGroup.get('value').disable({emitEvent: false});
}
}
private updateFunctionCodes(dataType: ModbusDataType): void {
this.functionCodes = dataType === ModbusDataType.BITS ? this.bitsFunctionCodes : this.defaultFunctionCodes;
if (!this.functionCodes.includes(this.rpcParametersFormGroup.get('functionCode').value)) {
this.rpcParametersFormGroup.get('functionCode').patchValue(this.functionCodes[0], {emitEvent: false});
}
}
}

19
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html

@ -16,16 +16,6 @@
--> -->
<div [formGroup]="slaveConfigFormGroup" class="slave-container"> <div [formGroup]="slaveConfigFormGroup" class="slave-container">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<div class="slave-content tb-form-panel no-border no-padding padding-top" > <div class="slave-content tb-form-panel no-border no-padding padding-top" >
<div class="tb-flex row space-between align-center no-gap fill-width"> <div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-slave-config</div> <div class="fixed-title-width" translate>gateway.server-slave-config</div>
@ -161,7 +151,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.poll-period</div> <div class="fixed-title-width tb-required" translate>gateway.poll-period</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
@ -178,6 +168,13 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.send-data-TB' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings"> <mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header> <mat-expansion-panel-header>

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss

@ -1,27 +0,0 @@
/**
* 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.
*/
$server-config-header-height: 132px;
:host {
.slave-content {
height: calc(100% - #{$server-config-header-height});
overflow: auto;
}
.slave-container {
display: inherit;
}
}

41
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -43,7 +43,7 @@ import {
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
@ -73,7 +73,6 @@ import { isEqual } from '@core/utils';
ModbusSecurityConfigComponent, ModbusSecurityConfigComponent,
GatewayPortTooltipPipe, GatewayPortTooltipPipe,
], ],
styleUrls: ['./modbus-slave-config.component.scss'],
}) })
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@ -90,6 +89,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
readonly ModbusProtocolType = ModbusProtocolType; readonly ModbusProtocolType = ModbusProtocolType;
readonly modbusBaudrates = ModbusBaudrates; readonly modbusBaudrates = ModbusBaudrates;
private isSlaveEnabled = false;
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; private readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
@ -106,11 +106,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET], method: [ModbusMethodType.SOCKET],
unitId: [0, [Validators.required]], unitId: [null, [Validators.required]],
baudrate: [this.modbusBaudrates[0]], baudrate: [this.modbusBaudrates[0]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
pollPeriod: [5000], pollPeriod: [5000, [Validators.required]],
sendDataToThingsBoard: [false], sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG], byteOrder:[ModbusOrderType.BIG],
security: [], security: [],
@ -126,14 +126,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
this.observeValueChanges(); this.observeValueChanges();
this.observeTypeChange(); this.observeTypeChange();
this.observeFormEnable();
this.observeShowSecurity(); this.observeShowSecurity();
} }
get isSlaveEnabled(): boolean {
return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value;
}
get protocolType(): ModbusProtocolType { get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value; return this.slaveConfigFormGroup.get('type').value;
} }
@ -160,7 +155,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
writeValue(slaveConfig: ModbusSlave): void { writeValue(slaveConfig: ModbusSlave): void {
this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {}));
this.updateSlaveConfig(slaveConfig); this.updateSlaveConfig(slaveConfig);
this.updateFormEnableState(slaveConfig.sendDataToThingsBoard); }
setDisabledState(isDisabled: boolean): void {
this.isSlaveEnabled = !isDisabled;
this.updateFormEnableState();
} }
private observeValueChanges(): void { private observeValueChanges(): void {
@ -180,7 +179,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
this.slaveConfigFormGroup.get('type').valueChanges this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(type => { .subscribe(type => {
this.updateFormEnableState(this.isSlaveEnabled); this.updateFormEnableState();
this.updateMethodType(type); this.updateMethodType(type);
}); });
} }
@ -196,22 +195,15 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
} }
} }
private observeFormEnable(): void { private updateFormEnableState(): void {
this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges if (this.isSlaveEnabled) {
.pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$))
.subscribe(value => this.updateFormEnableState(value));
}
private updateFormEnableState(enabled: boolean): void {
if (enabled) {
this.slaveConfigFormGroup.enable({emitEvent: false}); this.slaveConfigFormGroup.enable({emitEvent: false});
this.showSecurityControl.enable({emitEvent: false}); this.showSecurityControl.enable({emitEvent: false});
} else { } else {
this.slaveConfigFormGroup.disable({emitEvent: false}); this.slaveConfigFormGroup.disable({emitEvent: false});
this.showSecurityControl.disable({emitEvent: false}); this.showSecurityControl.disable({emitEvent: false});
this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false});
} }
this.updateEnablingByProtocol(this.protocolType); this.updateEnablingByProtocol();
this.updateSecurityEnable(this.showSecurityControl.value); this.updateSecurityEnable(this.showSecurityControl.value);
} }
@ -229,9 +221,10 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
} }
} }
private updateEnablingByProtocol(type: ModbusProtocolType): void { private updateEnablingByProtocol(): void {
const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; const isSerial = this.protocolType === ModbusProtocolType.Serial;
const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; const enableKeys = isSerial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys;
const disableKeys = isSerial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys;
if (this.isSlaveEnabled) { if (this.isSlaveEnabled) {
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -28,7 +28,7 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel"> <div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div> <div class="fixed-title-width slave-name-label tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
@ -306,7 +306,7 @@
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.poll-period</div> <div class="fixed-title-width-230 tb-required" translate>gateway.poll-period</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
@ -314,7 +314,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-time</div> <div class="fixed-title-width-230 tb-required" translate>gateway.connect-attempt-time</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/>
@ -322,7 +322,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-count</div> <div class="fixed-title-width-230 tb-required" translate>gateway.connect-attempt-count</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/>
@ -330,7 +330,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.wait-after-failed-attempts</div> <div class="fixed-title-width-230 tb-required" translate>gateway.wait-after-failed-attempts</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/>

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss

@ -18,4 +18,9 @@
width: 80vw; width: 80vw;
max-width: 900px; max-width: 900px;
} }
.slave-name-label {
margin-right: 16px;
color: rgba(0, 0, 0, 0.87);
}
} }

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -163,7 +163,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
bytesize: [ModbusByteSizes[0]], bytesize: [ModbusByteSizes[0]],
parity: [ModbusParity.None], parity: [ModbusParity.None],
strict: [true], strict: [true],
unitId: [0, [Validators.required]], unitId: [null, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false], sendDataOnlyOnChange: [false],
@ -173,10 +173,10 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
retries: [true], retries: [true],
retryOnEmpty: [true], retryOnEmpty: [true],
retryOnInvalid: [true], retryOnInvalid: [true],
pollPeriod: [5000], pollPeriod: [5000, [Validators.required]],
connectAttemptTimeMs: [5000], connectAttemptTimeMs: [5000, [Validators.required]],
connectAttemptCount: [5], connectAttemptCount: [5, [Validators.required]],
waitAfterFailedAttemptsMs: [300000], waitAfterFailedAttemptsMs: [300000, [Validators.required]],
values: [{}], values: [{}],
security: [{}], security: [{}],
}); });

7
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss

@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host {
.mat-mdc-tab-body-wrapper { :host ::ng-deep .mat-mdc-tab-body-wrapper {
min-height: 320px; min-height: 320px;
}
} }
::ng-deep .mdc-evolution-chip-set__chips { ::ng-deep .mdc-evolution-chip-set__chips {

39
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts

@ -47,6 +47,7 @@ import {
import { import {
MappingTableComponent MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import { isDefinedAndNotNull } from '@core/utils';
@Component({ @Component({
selector: 'tb-mqtt-basic-config', selector: 'tb-mqtt-basic-config',
@ -84,7 +85,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
mappingTypes = MappingType; mappingTypes = MappingType;
basicFormGroup: FormGroup; basicFormGroup: FormGroup;
private onChange: (value: string) => void; private onChange: (value: MQTTBasicConfig) => void;
private onTouched: () => void; private onTouched: () => void;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -100,7 +101,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.basicFormGroup.valueChanges this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(value => { .subscribe(value => {
this.onChange(value); this.onChange(this.getMappedMQTTConfig(value));
this.onTouched(); this.onTouched();
}); });
} }
@ -110,7 +111,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.destroy$.complete(); this.destroy$.complete();
} }
registerOnChange(fn: (value: string) => void): void { registerOnChange(fn: (value: MQTTBasicConfig) => void): void {
this.onChange = fn; this.onChange = fn;
} }
@ -135,13 +136,30 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.basicFormGroup.setValue(editedBase, {emitEvent: false}); this.basicFormGroup.setValue(editedBase, {emitEvent: false});
} }
private getMappedMQTTConfig(basicConfig: MQTTBasicConfig): MQTTBasicConfig {
let { broker, workers, dataMapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) {
broker = {
...broker,
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingData[]);
}
return { broker, workers, dataMapping, requestsMapping };
}
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false} basicFormGroup: {valid: false}
}; };
} }
private getRequestDataArray(value: Record<RequestType, RequestMappingData>): RequestMappingData[] { private getRequestDataArray(value: Record<RequestType, RequestMappingData[]>): RequestMappingData[] {
const mappingConfigs = []; const mappingConfigs = [];
if (isObject(value)) { if (isObject(value)) {
@ -157,4 +175,17 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
return mappingConfigs; return mappingConfigs;
} }
private getRequestDataObject(array: RequestMappingData[]): Record<RequestType, RequestMappingData[]> {
return array.reduce((result, { requestType, requestValue }) => {
result[requestType].push(requestValue);
return result;
}, {
connectRequests: [],
disconnectRequests: [],
attributeRequests: [],
attributeUpdates: [],
serverSideRpc: [],
});
}
} }

31
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts

@ -85,7 +85,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
subCheckPeriodInMillis: [10, [Validators.required, Validators.min(10)]], subCheckPeriodInMillis: [10, [Validators.required, Validators.min(10)]],
showMap: [false, []], showMap: [false, []],
security: [SecurityPolicy.BASIC128, []], security: [SecurityPolicy.BASIC128, []],
identity: [{}, [Validators.required]] identity: []
}); });
this.serverConfigFormGroup.valueChanges.pipe( this.serverConfigFormGroup.valueChanges.pipe(
@ -116,16 +116,25 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
} }
writeValue(serverConfig: ServerConfig): void { writeValue(serverConfig: ServerConfig): void {
const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig; const {
const serverConfigState = { timeoutInMillis = 1000,
scanPeriodInMillis = 1000,
enableSubscriptions = true,
subCheckPeriodInMillis = 10,
showMap = false,
security = SecurityPolicy.BASIC128,
identity = {},
} = serverConfig;
this.serverConfigFormGroup.reset({
...serverConfig, ...serverConfig,
timeoutInMillis: timeoutInMillis || 1000, timeoutInMillis,
scanPeriodInMillis: scanPeriodInMillis || 1000, scanPeriodInMillis,
enableSubscriptions: enableSubscriptions || true, enableSubscriptions,
subCheckPeriodInMillis: subCheckPeriodInMillis || 10, subCheckPeriodInMillis,
showMap: showMap || false, showMap,
security: security || SecurityPolicy.BASIC128, security,
}; identity,
this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false}); }, { emitEvent: false });
} }
} }

77
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -51,6 +51,7 @@ import { EntityType } from '@shared/models/entity-type.models';
import { import {
AddConnectorConfigData, AddConnectorConfigData,
ConnectorBaseConfig, ConnectorBaseConfig,
ConnectorBaseInfo,
ConnectorConfigurationModes, ConnectorConfigurationModes,
ConnectorType, ConnectorType,
GatewayConnector, GatewayConnector,
@ -60,7 +61,7 @@ import {
} from './gateway-widget.models'; } from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { debounceTime, take, takeUntil, tap } from 'rxjs/operators'; import { debounceTime, filter, take, takeUntil, tap } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
@ -133,6 +134,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private basicConfigSub: Subscription; private basicConfigSub: Subscription;
private jsonConfigSub: Subscription;
private subscriptionOptions: WidgetSubscriptionOptions = { private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: { callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => { onDataUpdated: () => this.ctx.ngZone.run(() => {
@ -209,17 +212,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
}); });
this.connectorForm.get('configurationJson').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const basicConfig = this.connectorForm.get('basicConfig');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
}
});
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
switch (sortHeaderId) { switch (sortHeaderId) {
@ -275,7 +267,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
saveConnector(): void { saveConnector(): void {
const value = this.connectorForm.get('type').value === ConnectorType.MQTT ? this.getMappedMQTTValue() : this.connectorForm.value; const value = { ...this.connectorForm.value };
value.configuration = camelCase(value.name) + '.json'; value.configuration = camelCase(value.name) + '.json';
delete value.basicConfig; delete value.basicConfig;
if (value.type !== ConnectorType.GRPC) { if (value.type !== ConnectorType.GRPC) {
@ -336,20 +328,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
private getMappedMQTTValue(): GatewayConnector {
const value = this.connectorForm.value;
return {
...value,
configurationJson: {
...value.configurationJson,
broker: {
...value.configurationJson.broker,
...value.configurationJson.workers,
}
}
};
}
private updateData(reload: boolean = false): void { private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
@ -383,11 +361,28 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
const sharedIndex = this.sharedAttributeData.findIndex(data => { const sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value; const sharedData = data.value;
return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts; const hasSameName = sharedData.name === connectorData.name;
const hasEmptyConfig = isEqual(sharedData.configurationJson, {}) && hasSameName;
const hasSameConfig = this.hasSameConfig(sharedData.configurationJson, connectorData.configurationJson);
const isRecentlyCreated = sharedData.ts && sharedData.ts <= connectorData.ts;
return hasSameName && isRecentlyCreated && (hasSameConfig || hasEmptyConfig);
}); });
return sharedIndex !== -1; return sharedIndex !== -1;
} }
private hasSameConfig(sharedDataConfigJson: ConnectorBaseInfo, connectorDataConfigJson: ConnectorBaseInfo): boolean {
const { name, id, enableRemoteLogging, logLevel, ...sharedDataConfig } = sharedDataConfigJson;
const {
name: connectorName,
id: connectorId,
enableRemoteLogging: connectorEnableRemoteLogging,
logLevel: connectorLogLevel,
...connectorConfig
} = connectorDataConfigJson;
return isEqual(sharedDataConfig, connectorConfig);
}
private combineData(): void { private combineData(): void {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key) index === self.findIndex((t) => t.key === item.key)
@ -684,6 +679,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.basicConfigSub.unsubscribe(); this.basicConfigSub.unsubscribe();
} }
this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe( this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe(
filter(() => !!this.initialConnector),
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe((config) => { ).subscribe((config) => {
const configJson = this.connectorForm.get('configurationJson'); const configJson = this.connectorForm.get('configurationJson');
@ -696,6 +692,22 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
private createJsonConfigWatcher(): void {
if (this.jsonConfigSub) {
this.jsonConfigSub.unsubscribe();
}
this.jsonConfigSub = this.connectorForm.get('configurationJson').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const basicConfig = this.connectorForm.get('basicConfig');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
}
});
}
private confirmConnectorChange(): Observable<boolean> { private confirmConnectorChange(): Observable<boolean> {
if (this.initialConnector && this.connectorForm.dirty) { if (this.initialConnector && this.connectorForm.dirty) {
return this.dialogService.confirm( return this.dialogService.confirm(
@ -734,19 +746,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
case ConnectorType.MQTT: case ConnectorType.MQTT:
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.MODBUS: case ConnectorType.MODBUS:
this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true}); this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false});
setTimeout(() => { setTimeout(() => {
this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC}); this.connectorForm.patchValue(connector, {emitEvent: false});
this.createBasicConfigWatcher();
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
}); });
break; break;
default: default:
this.connectorForm.patchValue({...connector, mode: null}); this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
this.createJsonConfigWatcher();
} }
private setClientData(data: PageData<AttributeData>): void { private setClientData(data: PageData<AttributeData>): void {

40
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html

@ -50,46 +50,6 @@
placeholder="${params}"/> placeholder="${params}"/>
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="ConnectorType.MODBUS">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.tag' | translate }}</mat-label>
<input matInput formControlName="tag" placeholder="setValue"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusCommandTypes" [value]="type">
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.functionCode' | translate }}</mat-label>
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of codesArray" [value]="code">
{{ modbusCodesTranslate.get(code) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-form-field *ngIf="commandForm.get('functionCode').value>4">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<input matInput formControlName="value" placeholder="value"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.address' | translate }}</mat-label>
<input matInput formControlName="address" type="number" min="0"
placeholder="1" step="1"/>
</mat-form-field>
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.objectsCount' | translate }}</mat-label>
<input matInput formControlName="objectsCount" type="number" min="0"
placeholder="31" step="1"/>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.BACNET"> <ng-template [ngSwitchCase]="ConnectorType.BACNET">
<mat-form-field> <mat-form-field>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label> <mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts

@ -35,8 +35,6 @@ import {
ConnectorType, ConnectorType,
GatewayConnectorDefaultTypesTranslatesMap, GatewayConnectorDefaultTypesTranslatesMap,
HTTPMethods, HTTPMethods,
ModbusCodesTranslate,
ModbusCommandTypes,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
RPCCommand, RPCCommand,
RPCTemplateConfig, RPCTemplateConfig,
@ -53,7 +51,7 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component'; import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { filter, takeUntil, tap } from "rxjs/operators"; import { takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
@Component({ @Component({
@ -80,9 +78,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter(); saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup; commandForm: FormGroup;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
ConnectorType = ConnectorType; ConnectorType = ConnectorType;
modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[];
bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[]; bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[];
bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[]; bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[];
bLEMethods = Object.values(BLEMethods) as BLEMethods[]; bLEMethods = Object.values(BLEMethods) as BLEMethods[];
@ -98,7 +94,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates; SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates;
SNMPMethodsTranslations = SNMPMethodsTranslations; SNMPMethodsTranslations = SNMPMethodsTranslations;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap; gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
modbusCodesTranslate = ModbusCodesTranslate;
urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/; urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/;
numbersOnlyPattern = /^[0-9]*$/; numbersOnlyPattern = /^[0-9]*$/;
@ -156,26 +151,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
withResponse: [false, []], withResponse: [false, []],
}); });
break; break;
case ConnectorType.MODBUS:
formGroup = this.fb.group({
tag: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [null, [Validators.required]],
functionCode: [null, [Validators.required]],
value: [null, []],
address: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]],
objectsCount: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]]
})
const valueForm = formGroup.get('value');
formGroup.get('functionCode').valueChanges.subscribe(value => {
if (value > 4) {
valueForm.addValidators([Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]);
} else {
valueForm.clearValidators();
valueForm.setValue(null);
}
valueForm.updateValueAndValidity();
})
break;
case ConnectorType.BACNET: case ConnectorType.BACNET:
formGroup = this.fb.group({ formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html

@ -41,8 +41,32 @@
</button> </button>
</ng-container> </ng-container>
<ng-template #connectorForm> <ng-template #connectorForm>
<tb-gateway-service-rpc-connector formControlName="params" [connectorType]="connectorType" <tb-gateway-service-rpc-connector
(sendCommand)="sendCommand()" (saveTemplate)="saveTemplate()"/> *ngIf="connectorType !== ConnectorType.MODBUS else modbusParameters"
formControlName="params"
[connectorType]="connectorType"
(sendCommand)="sendCommand()"
(saveTemplate)="saveTemplate()"
/>
<ng-template #modbusParameters>
<div fxLayout="column" class="rpc-parameters">
<div class="mat-subtitle-1 tb-form-panel-title">{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}</div>
<tb-modbus-rpc-parameters formControlName="params"/>
<div class="template-actions" fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="10px">
<button mat-raised-button
(click)="saveTemplate()"
[disabled]="commandForm.get('params').invalid">
{{ 'gateway.rpc-command-save-template' | translate }}
</button>
<button mat-raised-button
color="primary"
(click)="sendCommand()"
[disabled]="commandForm.get('params').invalid">
{{ 'gateway.rpc-command-send' | translate }}
</button>
</div>
</div>
</ng-template>
</ng-template> </ng-template>
</div> </div>
<section class="result-block" [formGroup]="commandForm"> <section class="result-block" [formGroup]="commandForm">

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss

@ -40,6 +40,10 @@
} }
} }
.rpc-parameters {
width: 100%;
}
.result-block { .result-block {
padding: 0 5px; padding: 0 5px;
display: flex; display: flex;

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -22,6 +22,7 @@ import { ContentType } from '@shared/models/constants';
import { jsonRequired } from '@shared/components/json-object-edit.component'; import { jsonRequired } from '@shared/components/json-object-edit.component';
import { import {
ConnectorType, ConnectorType,
GatewayConnectorDefaultTypesTranslatesMap,
RPCCommand, RPCCommand,
RPCTemplate, RPCTemplate,
RPCTemplateConfig, RPCTemplateConfig,
@ -71,6 +72,9 @@ export class GatewayServiceRPCComponent implements OnInit {
public connectorType: ConnectorType; public connectorType: ConnectorType;
public templates: Array<RPCTemplate> = []; public templates: Array<RPCTemplate> = [];
readonly ConnectorType = ConnectorType;
readonly gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private subscriptionOptions: WidgetSubscriptionOptions = { private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: { callbacks: {

67
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -170,19 +170,27 @@ export interface ConnectorSecurity {
pathToCACert?: string; pathToCACert?: string;
pathToPrivateKey?: string; pathToPrivateKey?: string;
pathToClientCert?: string; pathToClientCert?: string;
mode?: ModeType;
} }
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping; export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping;
export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue; export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue;
export type ConnectorBaseConfig = MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig; export type ConnectorBaseConfig = ConnectorBaseInfo | MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig;
export interface ConnectorBaseInfo {
name: string;
id: string;
enableRemoteLogging: boolean;
logLevel: GatewayLogLevel;
}
export interface MQTTBasicConfig { export interface MQTTBasicConfig {
dataMapping: ConverterConnectorMapping[]; dataMapping: ConverterConnectorMapping[];
requestsMapping: Record<RequestType, RequestMappingData> | RequestMappingData[]; requestsMapping: Record<RequestType, RequestMappingData[]> | RequestMappingData[];
broker: BrokerConfig; broker: BrokerConfig;
workers: WorkersConfig; workers?: WorkersConfig;
} }
export interface OPCBasicConfig { export interface OPCBasicConfig {
@ -311,35 +319,15 @@ export interface RPCCommand {
time: number; time: number;
} }
export const ModbusFunctionCodeTranslationsMap = new Map<number, string>([
export enum ModbusCommandTypes { [1, 'gateway.function-codes.read-coils'],
Bits = 'bits', [2, 'gateway.function-codes.read-discrete-inputs'],
Bit = 'bit', [3, 'gateway.function-codes.read-multiple-holding-registers'],
// eslint-disable-next-line id-blacklist [4, 'gateway.function-codes.read-input-registers'],
String = 'string', [5, 'gateway.function-codes.write-single-coil'],
Bytes = 'bytes', [6, 'gateway.function-codes.write-single-holding-register'],
Int8 = '8int', [15, 'gateway.function-codes.write-multiple-coils'],
Uint8 = '8uint', [16, 'gateway.function-codes.write-multiple-holding-registers']
Int16 = '16int',
Uint16 = '16uint',
Float16 = '16float',
Int32 = '32int',
Uint32 = '32uint',
Float32 = '32float',
Int64 = '64int',
Uint64 = '64uint',
Float64 = '64float'
}
export const ModbusCodesTranslate = new Map<number, string>([
[1, 'gateway.rpc.read-coils'],
[2, 'gateway.rpc.read-discrete-inputs'],
[3, 'gateway.rpc.read-multiple-holding-registers'],
[4, 'gateway.rpc.read-input-registers'],
[5, 'gateway.rpc.write-single-coil'],
[6, 'gateway.rpc.write-single-holding-register'],
[15, 'gateway.rpc.write-multiple-coils'],
[16, 'gateway.rpc.write-multiple-holding-registers']
]); ]);
export enum BACnetRequestTypes { export enum BACnetRequestTypes {
@ -862,6 +850,8 @@ export enum ModbusDataType {
FLOAT64 = '64float' FLOAT64 = '64float'
} }
export const ModbusEditableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
export enum ModbusObjectCountByDataType { export enum ModbusObjectCountByDataType {
'8int' = 1, '8int' = 1,
'8uint' = 1, '8uint' = 1,
@ -920,19 +910,6 @@ export const ModbusKeysNoKeysTextTranslationsMap = new Map<ModbusValueKey, strin
] ]
); );
export const ModbusFunctionCodeTranslationsMap = new Map<number, string>(
[
[1, 'gateway.read-coils'],
[2, 'gateway.read-discrete-inputs'],
[3, 'gateway.read-multiple-holding-registers'],
[4, 'gateway.read-input-registers'],
[5, 'gateway.write-coil'],
[6, 'gateway.write-register'],
[15, 'gateway.write-coils'],
[16, 'gateway.write-registers'],
]
);
export interface ModbusMasterConfig { export interface ModbusMasterConfig {
slaves: SlaveConfig[]; slaves: SlaveConfig[];
} }

4
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -141,6 +141,9 @@ import {
import { import {
TypeValuePanelComponent TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component';
import {
ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -227,6 +230,7 @@ import {
KeyValueIsNotEmptyPipe, KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent, ModbusBasicConfigComponent,
EllipsisChipListDirective, EllipsisChipListDirective,
ModbusRpcParametersComponent,
], ],
exports: [ exports: [
EntitiesTableWidgetComponent, EntitiesTableWidgetComponent,

18
ui-ngx/src/app/shared/directives/public-api.ts

@ -0,0 +1,18 @@
///
/// 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.
///
export * from './truncate-with-tooltip.directive';
export * from './ellipsis-chip-list.directive';

2
ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts

@ -105,8 +105,6 @@ export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDe
private showTooltip(): void { private showTooltip(): void {
this.tooltip.message = this.text; this.tooltip.message = this.text;
this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text);
this.tooltip.show(); this.tooltip.show();
} }

1
ui-ngx/src/app/shared/public-api.ts

@ -19,3 +19,4 @@ export * from './decorators/public-api';
export * from './models/public-api'; export * from './models/public-api';
export * from './pipe/public-api'; export * from './pipe/public-api';
export * from './shared.module'; export * from './shared.module';
export * from './directives/public-api';

25
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2915,6 +2915,16 @@
"from-device-request-settings": "Input request parsing", "from-device-request-settings": "Input request parsing",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.", "from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"function-code": "Function code", "function-code": "Function code",
"function-codes": {
"read-coils": "01 - Read Coils",
"read-discrete-inputs": "02 - Read Discrete Inputs",
"read-multiple-holding-registers": "03 - Read Multiple Holding Registers",
"read-input-registers": "04 - Read Input Registers",
"write-single-coil": "05 - Write Single Coil",
"write-single-holding-register": "06 - Write Single Holding Register",
"write-multiple-coils": "15 - Write Multiple Coils",
"write-multiple-holding-registers": "16 - Write Multiple Holding Registers"
},
"to-device-response-settings": "Output request processing", "to-device-response-settings": "Output request processing",
"to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}", "to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway", "gateway": "Gateway",
@ -3121,14 +3131,6 @@
"template-name-duplicate": "Template with such name already exists, it will be updated.", "template-name-duplicate": "Template with such name already exists, it will be updated.",
"command": "Command", "command": "Command",
"params": "Params", "params": "Params",
"read-coils": "01: Read Coils",
"read-discrete-inputs": "02: Read Discrete Inputs",
"read-multiple-holding-registers": "03: Read Multiple Holding Registers",
"read-input-registers": "04: Read Input Registers",
"write-single-coil": "05: Write Single Coil",
"write-single-holding-register": "06: Write Single Holding Register",
"write-multiple-coils": "15: Write Multiple Coils",
"write-multiple-holding-registers": "16: Write Multiple Holding Registers",
"json-value-invalid": "JSON value has an invalid format" "json-value-invalid": "JSON value has an invalid format"
}, },
"rpc-methods": "RPC methods", "rpc-methods": "RPC methods",
@ -3318,16 +3320,13 @@
"username": "Username", "username": "Username",
"username-required": "Username is required.", "username-required": "Username is required.",
"unit-id-required": "Unit ID is required.", "unit-id-required": "Unit ID is required.",
"read-coils": "Read Coils",
"read-discrete-inputs": "Read Discrete Inputs",
"read-multiple-holding-registers": "Read Multiple Holding Register",
"read-input-registers": "Read Input Registers",
"write-coil": "Write Coil", "write-coil": "Write Coil",
"write-coils": "Write Coils", "write-coils": "Write Coils",
"write-register": "Write Register", "write-register": "Write Register",
"write-registers": "Write Registers", "write-registers": "Write Registers",
"hints": { "hints": {
"modbus-server": "Starting with version 3.0, Gateway can run as a Modbus slave.", "modbus-master": "Configuration sections for connecting to Modbus servers and reading data from them.",
"modbus-server": "Configuration section for the Modbus server, storing data and sending updates to the platform when changes occur or at fixed intervals.",
"remote-configuration": "Enables remote configuration and management of the gateway", "remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of platform server", "host": "Hostname or IP address of platform server",

Loading…
Cancel
Save