39 changed files with 3135 additions and 151 deletions
@ -0,0 +1,28 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-tab-group [formGroup]="basicFormGroup"> |
||||
|
<mat-tab label="{{ 'gateway.general' | translate }}"> |
||||
|
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container> |
||||
|
</mat-tab> |
||||
|
<mat-tab label="{{ 'gateway.master-connections' | translate }}"> |
||||
|
<tb-modbus-master-table formControlName="master"></tb-modbus-master-table> |
||||
|
</mat-tab> |
||||
|
<mat-tab label="{{ 'gateway.server-config' | translate }}"> |
||||
|
<tb-modbus-server-config formControlName="slave"></tb-modbus-server-config> |
||||
|
</mat-tab> |
||||
|
</mat-tab-group> |
||||
@ -0,0 +1,126 @@ |
|||||
|
///
|
||||
|
/// 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, Input, OnDestroy, TemplateRef } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
FormGroup, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
} from '@angular/forms'; |
||||
|
import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
|
||||
|
import { EllipsisChipListDirective } from '@shared/directives/public-api'; |
||||
|
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; |
||||
|
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-basic-config', |
||||
|
templateUrl: './modbus-basic-config.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ModbusBasicConfigComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => ModbusBasicConfigComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
standalone: true, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
ModbusSlaveConfigComponent, |
||||
|
ModbusMasterTableComponent, |
||||
|
EllipsisChipListDirective, |
||||
|
], |
||||
|
styles: [` |
||||
|
:host { |
||||
|
height: 100%; |
||||
|
} |
||||
|
:host ::ng-deep { |
||||
|
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper { |
||||
|
height: 100%; |
||||
|
} |
||||
|
} |
||||
|
`]
|
||||
|
}) |
||||
|
|
||||
|
export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy { |
||||
|
|
||||
|
@Input() generalTabContent: TemplateRef<any>; |
||||
|
|
||||
|
basicFormGroup: FormGroup; |
||||
|
|
||||
|
onChange: (value: string) => void; |
||||
|
onTouched: () => void; |
||||
|
|
||||
|
protected readonly connectorType = ConnectorType; |
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
this.basicFormGroup = this.fb.group({ |
||||
|
master: [], |
||||
|
slave: [], |
||||
|
}); |
||||
|
|
||||
|
this.basicFormGroup.valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(value => { |
||||
|
this.onChange(value); |
||||
|
this.onTouched(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: string) => void): void { |
||||
|
this.onChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: () => void): void { |
||||
|
this.onTouched = fn; |
||||
|
} |
||||
|
|
||||
|
writeValue(basicConfig: ModbusBasicConfig): void { |
||||
|
const editedBase = { |
||||
|
slave: basicConfig.slave || {}, |
||||
|
master: basicConfig.master || {}, |
||||
|
}; |
||||
|
|
||||
|
this.basicFormGroup.setValue(editedBase, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.basicFormGroup.valid ? null : { |
||||
|
basicFormGroup: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,165 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-modbus-keys-panel"> |
||||
|
<div class="tb-form-panel no-border no-padding"> |
||||
|
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div> |
||||
|
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys"> |
||||
|
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width" |
||||
|
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByKey; let $index = index; let last = last;"> |
||||
|
<div class="tb-form-panel stroked tb-flex"> |
||||
|
<ng-container [formGroup]="keyControl"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="last"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<div class="title-container"> |
||||
|
<span *ngIf="isMaster else tagName"> |
||||
|
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }} |
||||
|
</span> |
||||
|
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template> |
||||
|
</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.platform-side</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate> |
||||
|
gateway.key |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<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="keyControl.get('tag').hasError('required') && |
||||
|
keyControl.get('tag').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.connector-side</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate> |
||||
|
gateway.type |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="type"> |
||||
|
<mat-option *ngFor="let type of modbusDataTypes" [value]="type">{{ type }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="withFunctionCode" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.function-code</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="functionCode"> |
||||
|
<mat-option *ngFor="let code of functionCodesMap.get(keyControl.get('id').value) || defaultFunctionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.objects-count</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input |
||||
|
matInput |
||||
|
type="number" |
||||
|
min="1" |
||||
|
name="value" |
||||
|
formControlName="objectsCount" |
||||
|
placeholder="{{ 'gateway.set' | translate }}" |
||||
|
[readonly]="!editableDataTypes.includes(keyControl.get('type').value)" |
||||
|
/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.address</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="isMaster" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.value</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<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="keyControl.get('value').hasError('required') && |
||||
|
keyControl.get('value').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
(click)="deleteKey($event, $index)" |
||||
|
[matTooltip]="deleteKeyTitle | translate" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div> |
||||
|
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> |
||||
|
{{ addKeyTitle | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #noKeys> |
||||
|
<div class="tb-flex no-flex center align-center key-panel"> |
||||
|
<span class="tb-prompt" translate>{{ noKeysText }}</span> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<div class="tb-flex flex-end"> |
||||
|
<button mat-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="applyKeysData()" |
||||
|
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,44 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
.tb-modbus-keys-panel { |
||||
|
width: 77vw; |
||||
|
max-width: 700px; |
||||
|
|
||||
|
.title-container { |
||||
|
max-width: 11vw; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap |
||||
|
} |
||||
|
|
||||
|
.key-panel { |
||||
|
height: 500px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
.tb-form-panel { |
||||
|
.mat-mdc-icon-button { |
||||
|
width: 56px; |
||||
|
height: 56px; |
||||
|
padding: 16px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,172 @@ |
|||||
|
///
|
||||
|
/// 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 { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
||||
|
import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { |
||||
|
ModbusDataType, |
||||
|
ModbusFunctionCodeTranslationsMap, |
||||
|
ModbusObjectCountByDataType, |
||||
|
ModbusRegisterType, |
||||
|
ModbusValue, |
||||
|
ModbusValueKey, |
||||
|
noLeadTrailSpacesRegex, |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; |
||||
|
import { generateSecret } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-data-keys-panel', |
||||
|
templateUrl: './modbus-data-keys-panel.component.html', |
||||
|
styleUrls: ['./modbus-data-keys-panel.component.scss'], |
||||
|
standalone: true, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
GatewayHelpLinkPipe, |
||||
|
] |
||||
|
}) |
||||
|
export class ModbusDataKeysPanelComponent implements OnInit { |
||||
|
|
||||
|
@Input() isMaster = false; |
||||
|
@Input() panelTitle: string; |
||||
|
@Input() addKeyTitle: string; |
||||
|
@Input() deleteKeyTitle: string; |
||||
|
@Input() noKeysText: string; |
||||
|
@Input() register: ModbusRegisterType; |
||||
|
@Input() keysType: ModbusValueKey; |
||||
|
@Input() values: ModbusValue[]; |
||||
|
@Input() popover: TbPopoverComponent<ModbusDataKeysPanelComponent>; |
||||
|
|
||||
|
@Output() keysDataApplied = new EventEmitter<Array<ModbusValue>>(); |
||||
|
|
||||
|
keysListFormArray: UntypedFormArray; |
||||
|
errorText = ''; |
||||
|
modbusDataTypes = Object.values(ModbusDataType); |
||||
|
withFunctionCode = true; |
||||
|
functionCodesMap = new Map(); |
||||
|
defaultFunctionCodes = []; |
||||
|
|
||||
|
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; |
||||
|
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; |
||||
|
readonly defaultReadFunctionCodes = [3, 4]; |
||||
|
readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.keysListFormArray = this.prepareKeysFormArray(this.values); |
||||
|
this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES); |
||||
|
this.defaultFunctionCodes = this.getDefaultFunctionCodes(); |
||||
|
} |
||||
|
|
||||
|
trackByKey(_: number, keyControl: AbstractControl): AbstractControl { |
||||
|
return keyControl; |
||||
|
} |
||||
|
|
||||
|
addKey(): void { |
||||
|
const dataKeyFormGroup = this.fb.group({ |
||||
|
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
type: [ModbusDataType.STRING, [Validators.required]], |
||||
|
address: [0, [Validators.required]], |
||||
|
objectsCount: [1, [Validators.required]], |
||||
|
functionCode: [this.getDefaultFunctionCodes()[0]], |
||||
|
id: [{value: generateSecret(5), disabled: true}], |
||||
|
}); |
||||
|
this.observeKeyDataType(dataKeyFormGroup); |
||||
|
|
||||
|
this.keysListFormArray.push(dataKeyFormGroup); |
||||
|
} |
||||
|
|
||||
|
deleteKey($event: Event, index: number): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.keysListFormArray.removeAt(index); |
||||
|
this.keysListFormArray.markAsDirty(); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.popover?.hide(); |
||||
|
} |
||||
|
|
||||
|
applyKeysData(): void { |
||||
|
this.keysDataApplied.emit(this.keysListFormArray.value); |
||||
|
} |
||||
|
|
||||
|
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray { |
||||
|
const keysControlGroups: Array<AbstractControl> = []; |
||||
|
if (values) { |
||||
|
values.forEach(keyData => { |
||||
|
const { tag, value, type, address, objectsCount, functionCode } = keyData; |
||||
|
const dataKeyFormGroup = this.fb.group({ |
||||
|
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
value: [{value, disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
type: [type, [Validators.required]], |
||||
|
address: [address, [Validators.required]], |
||||
|
objectsCount: [objectsCount, [Validators.required]], |
||||
|
functionCode: [functionCode, []], |
||||
|
id: [{value: generateSecret(5), disabled: true}], |
||||
|
}); |
||||
|
this.observeKeyDataType(dataKeyFormGroup); |
||||
|
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(type)); |
||||
|
|
||||
|
keysControlGroups.push(dataKeyFormGroup); |
||||
|
}); |
||||
|
} |
||||
|
return this.fb.array(keysControlGroups); |
||||
|
} |
||||
|
|
||||
|
private observeKeyDataType(keyFormGroup: FormGroup): void { |
||||
|
keyFormGroup.get('type').valueChanges.subscribe(dataType => { |
||||
|
const objectsCountControl = keyFormGroup.get('objectsCount'); |
||||
|
if (!this.editableDataTypes.includes(dataType)) { |
||||
|
objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]); |
||||
|
} |
||||
|
this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private getFunctionCodes(dataType: ModbusDataType): number[] { |
||||
|
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { |
||||
|
return this.defaultWriteFunctionCodes; |
||||
|
} |
||||
|
const functionCodes = [...this.defaultReadFunctionCodes]; |
||||
|
if (dataType === ModbusDataType.BITS) { |
||||
|
const bitsFunctionCodes = [1, 2]; |
||||
|
bitsFunctionCodes.forEach(code => functionCodes.push(code)); |
||||
|
functionCodes.sort(); |
||||
|
} |
||||
|
if (this.keysType === ModbusValueKey.RPC_REQUESTS) { |
||||
|
this.defaultWriteFunctionCodes.forEach(code => functionCodes.push(code)); |
||||
|
} |
||||
|
return functionCodes; |
||||
|
} |
||||
|
|
||||
|
private getDefaultFunctionCodes(): number[] { |
||||
|
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { |
||||
|
return this.defaultWriteFunctionCodes; |
||||
|
} |
||||
|
if (this.keysType === ModbusValueKey.RPC_REQUESTS) { |
||||
|
return [...this.defaultReadFunctionCodes, ...this.defaultWriteFunctionCodes]; |
||||
|
} |
||||
|
return this.defaultReadFunctionCodes; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-master-table tb-absolute-fill"> |
||||
|
<div fxFlex fxLayout="column" class="tb-master-table-content"> |
||||
|
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<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> |
||||
|
</div> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="manageSlave($event)" |
||||
|
matTooltip="{{ 'action.add' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>add</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
(click)="enterFilterMode()" |
||||
|
matTooltip="{{ 'action.search' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'action.search' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label> </mat-label> |
||||
|
<input #searchInput matInput |
||||
|
[formControl]="textSearch" |
||||
|
placeholder="{{ 'common.enter-search' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<button mat-icon-button (click)="exitFilterMode()" |
||||
|
matTooltip="{{ 'action.close' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<div class="table-container"> |
||||
|
<table mat-table [dataSource]="dataSource"> |
||||
|
<ng-container [matColumnDef]="'name'"> |
||||
|
<mat-header-cell *matHeaderCellDef class="table-value-column request-column"> |
||||
|
{{ 'gateway.name' | translate }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let mapping" class="table-value-column request-column"> |
||||
|
{{ mapping['name'] }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container [matColumnDef]="'type'"> |
||||
|
<mat-header-cell *matHeaderCellDef class="table-value-column request-column"> |
||||
|
{{ 'gateway.client-communication-type' | translate }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let mapping" class="table-value-column request-column"> |
||||
|
{{ ModbusClientTypeLabelsMap.get(mapping['type']) }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="actions" stickyEnd> |
||||
|
<mat-header-cell *matHeaderCellDef |
||||
|
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}"> |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let mapping; let i = index" |
||||
|
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> |
||||
|
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
||||
|
<button mat-icon-button |
||||
|
(click)="manageSlave($event, i)"> |
||||
|
<tb-icon>edit</tb-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
(click)="deleteMapping($event, i)"> |
||||
|
<tb-icon>delete</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> |
||||
|
<button mat-icon-button |
||||
|
(click)="$event.stopPropagation()" |
||||
|
[matMenuTriggerFor]="cellActionsMenu"> |
||||
|
<mat-icon class="material-icons">more_vert</mat-icon> |
||||
|
</button> |
||||
|
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
||||
|
<button mat-icon-button |
||||
|
(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> |
||||
|
</div> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<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> |
||||
|
</table> |
||||
|
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center" |
||||
|
class="mat-headline-5 tb-absolute-fill tb-add-new"> |
||||
|
<button mat-button class="connector" |
||||
|
(click)="manageSlave($event)"> |
||||
|
<mat-icon class="tb-mat-96">add</mat-icon> |
||||
|
<span>{{ 'gateway.add-slave' | translate }}</span> |
||||
|
</button> |
||||
|
</section> |
||||
|
</div> |
||||
|
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)" |
||||
|
fxLayoutAlign="center center" |
||||
|
class="no-data-found" translate> |
||||
|
widget.no-data-found |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,101 @@ |
|||||
|
/** |
||||
|
* 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 '../scss/constants'; |
||||
|
|
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: block; |
||||
|
.tb-master-table { |
||||
|
.tb-master-table-content { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: #fff; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
&.tb-outlined-border { |
||||
|
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); |
||||
|
border: solid 1px #e0e0e0; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.mat-toolbar-tools{ |
||||
|
min-height: auto; |
||||
|
} |
||||
|
|
||||
|
.title-container{ |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.tb-master-table-title { |
||||
|
padding-right: 20px; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.table-container { |
||||
|
overflow: auto; |
||||
|
.mat-mdc-table { |
||||
|
table-layout: fixed; |
||||
|
min-width: 450px; |
||||
|
|
||||
|
.table-value-column { |
||||
|
padding: 0 12px; |
||||
|
width: 23%; |
||||
|
|
||||
|
&.request-column { |
||||
|
width: 38%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ellipsis { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.no-data-found { |
||||
|
height: calc(100% - 120px); |
||||
|
} |
||||
|
|
||||
|
@media #{$mat-xs} { |
||||
|
.mat-toolbar { |
||||
|
height: auto; |
||||
|
min-height: 100px; |
||||
|
|
||||
|
.tb-master-table-title{ |
||||
|
padding-bottom: 5px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
mat-cell.tb-value-cell { |
||||
|
cursor: pointer; |
||||
|
.mat-icon { |
||||
|
height: 24px; |
||||
|
width: 24px; |
||||
|
font-size: 24px; |
||||
|
color: #757575 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
AfterViewInit, |
||||
|
ChangeDetectionStrategy, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
ViewChild, |
||||
|
} from '@angular/core'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormArray, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ModbusClientTypeLabelsMap, ModbusMasterConfig, SlaveConfig, |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { TbDatasource } from '@shared/abstract/datasource/datasource.abstract'; |
||||
|
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-master-table', |
||||
|
templateUrl: './modbus-master-table.component.html', |
||||
|
styleUrls: ['./modbus-master-table.component.scss'], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ModbusMasterTableComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => ModbusMasterTableComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
standalone: true, |
||||
|
imports: [CommonModule, SharedModule] |
||||
|
}) |
||||
|
export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { |
||||
|
|
||||
|
@ViewChild('searchInput') searchInputField: ElementRef; |
||||
|
|
||||
|
textSearchMode = false; |
||||
|
dataSource: SlavesDatasource; |
||||
|
hidePageSize = false; |
||||
|
activeValue = false; |
||||
|
dirtyValue = false; |
||||
|
masterFormGroup: UntypedFormGroup; |
||||
|
textSearch = this.fb.control('', {nonNullable: true}); |
||||
|
|
||||
|
readonly ModbusClientTypeLabelsMap = ModbusClientTypeLabelsMap; |
||||
|
|
||||
|
private onChange: (value: string) => void = () => {}; |
||||
|
private onTouched: () => void = () => {}; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor( |
||||
|
public translate: TranslateService, |
||||
|
public dialog: MatDialog, |
||||
|
private dialogService: DialogService, |
||||
|
private fb: FormBuilder |
||||
|
) { |
||||
|
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([])}); |
||||
|
this.dirtyValue = !this.activeValue; |
||||
|
this.dataSource = new SlavesDatasource(); |
||||
|
} |
||||
|
|
||||
|
get slaves(): FormArray { |
||||
|
return this.masterFormGroup.get('slaves') as FormArray; |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.masterFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value) => { |
||||
|
this.updateTableData(value.slaves); |
||||
|
this.onChange(value); |
||||
|
this.onTouched(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
this.textSearch.valueChanges.pipe( |
||||
|
debounceTime(150), |
||||
|
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((text) => { |
||||
|
const searchText = text.trim(); |
||||
|
this.updateTableData(this.slaves.value, searchText.trim()); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: string) => void): void { |
||||
|
this.onChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: () => void): void { |
||||
|
this.onTouched = fn; |
||||
|
} |
||||
|
|
||||
|
writeValue(master: ModbusMasterConfig): void { |
||||
|
this.slaves.clear(); |
||||
|
this.pushDataAsFormArrays(master.slaves); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.slaves.controls.length ? null : { |
||||
|
slavesFormGroup: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
enterFilterMode(): void { |
||||
|
this.textSearchMode = true; |
||||
|
setTimeout(() => { |
||||
|
this.searchInputField.nativeElement.focus(); |
||||
|
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
||||
|
}, 10); |
||||
|
} |
||||
|
|
||||
|
exitFilterMode(): void { |
||||
|
this.updateTableData(this.slaves.value); |
||||
|
this.textSearchMode = false; |
||||
|
this.textSearch.reset(); |
||||
|
} |
||||
|
|
||||
|
manageSlave($event: Event, index?: number): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const value = isDefinedAndNotNull(index) ? this.slaves.at(index).value : {}; |
||||
|
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
value, |
||||
|
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' |
||||
|
} |
||||
|
}).afterClosed() |
||||
|
.pipe(take(1), takeUntil(this.destroy$)) |
||||
|
.subscribe(res => { |
||||
|
if (res) { |
||||
|
if (isDefinedAndNotNull(index)) { |
||||
|
this.slaves.at(index).patchValue(res); |
||||
|
} else { |
||||
|
this.slaves.push(this.fb.control(res)); |
||||
|
} |
||||
|
this.masterFormGroup.markAsDirty(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
deleteMapping($event: Event, index: number): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialogService.confirm( |
||||
|
this.translate.instant('gateway.delete-slave-title'), |
||||
|
'', |
||||
|
this.translate.instant('action.no'), |
||||
|
this.translate.instant('action.yes'), |
||||
|
true |
||||
|
).subscribe((result) => { |
||||
|
if (result) { |
||||
|
this.slaves.removeAt(index); |
||||
|
this.masterFormGroup.markAsDirty(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateTableData(data: SlaveConfig[], textSearch?: string): void { |
||||
|
let tableValue = data; |
||||
|
if (textSearch) { |
||||
|
tableValue = tableValue.filter(value => |
||||
|
Object.values(value).some(val => |
||||
|
val.toString().toLowerCase().includes(textSearch.toLowerCase()) |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
this.dataSource.loadData(tableValue); |
||||
|
} |
||||
|
|
||||
|
private pushDataAsFormArrays(slaves: SlaveConfig[]): void { |
||||
|
if (slaves?.length) { |
||||
|
slaves.forEach((slave: SlaveConfig) => this.slaves.push(this.fb.control(slave))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class SlavesDatasource extends TbDatasource<SlaveConfig> { |
||||
|
constructor() { |
||||
|
super(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-form-panel no-border no-padding" [formGroup]="securityConfigFormGroup"> |
||||
|
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.hints.path-in-os' | translate }}</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.client-cert-path</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="certfile" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.private-key-path</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="keyfile" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.password</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<div class="tb-flex no-gap align-center fill-height" matSuffix> |
||||
|
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="!isMaster" class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.server-hostname</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="server_hostname" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="isMaster" class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="reqclicert"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.request-client-certificate' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,139 @@ |
|||||
|
///
|
||||
|
/// 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, Input, OnChanges, OnDestroy } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ModbusSecurity, |
||||
|
noLeadTrailSpacesRegex, |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-security-config', |
||||
|
templateUrl: './modbus-security-config.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ModbusSecurityConfigComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => ModbusSecurityConfigComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
standalone: true, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
SecurityConfigComponent, |
||||
|
] |
||||
|
}) |
||||
|
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { |
||||
|
|
||||
|
@Input() isMaster = false; |
||||
|
|
||||
|
securityConfigFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private onChange: (value: ModbusSecurity) => void; |
||||
|
private onTouched: () => void; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
this.securityConfigFormGroup = this.fb.group({ |
||||
|
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
}); |
||||
|
|
||||
|
this.observeValueChanges(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(): void { |
||||
|
if (this.isMaster) { |
||||
|
this.securityConfigFormGroup = this.fb.group({ |
||||
|
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
reqclicert: [false, []], |
||||
|
}); |
||||
|
this.observeValueChanges(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: ModbusSecurity) => void): void { |
||||
|
this.onChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: () => void): void { |
||||
|
this.onTouched = fn; |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.securityConfigFormGroup.valid ? null : { |
||||
|
serverConfigFormGroup: { valid: false } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(securityConfig: ModbusSecurity): void { |
||||
|
const { certfile, password, keyfile, server_hostname } = securityConfig; |
||||
|
let securityState = { |
||||
|
certfile: certfile ?? '', |
||||
|
password: password ?? '', |
||||
|
keyfile: keyfile ?? '', |
||||
|
server_hostname: server_hostname?? '', |
||||
|
reqclicert: !!securityConfig.reqclicert, |
||||
|
}; |
||||
|
if (this.isMaster) { |
||||
|
securityState = { ...securityState, reqclicert: !!securityConfig.reqclicert }; |
||||
|
} else { |
||||
|
securityState = { ...securityState, server_hostname: server_hostname ?? '' }; |
||||
|
} |
||||
|
this.securityConfigFormGroup.reset(securityState, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private observeValueChanges(): void { |
||||
|
this.securityConfigFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value: ModbusSecurity) => { |
||||
|
this.onChange(value); |
||||
|
this.onTouched(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,256 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="slaveConfigFormGroup"> |
||||
|
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div> |
||||
|
<div class="tb-flex row space-between align-center no-gap fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.server-slave-config</div> |
||||
|
<tb-toggle-select formControlName="type" appearance="fill"> |
||||
|
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-border no-padding padding-top"> |
||||
|
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial" |
||||
|
class="tb-form-row column-xs" |
||||
|
fxLayoutAlign="space-between center" |
||||
|
> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.host</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.host-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('host').hasError('required') |
||||
|
&& slaveConfigFormGroup.get('host').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial else serialPort" |
||||
|
class="tb-form-row column-xs" |
||||
|
fxLayoutAlign="space-between center" |
||||
|
> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}" |
||||
|
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip" |
||||
|
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') || |
||||
|
slaveConfigFormGroup.get('port').hasError('min') || |
||||
|
slaveConfigFormGroup.get('port').hasError('max')) && |
||||
|
slaveConfigFormGroup.get('port').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #serialPort> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip" |
||||
|
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') || |
||||
|
slaveConfigFormGroup.get('serialPort').hasError('min') || |
||||
|
slaveConfigFormGroup.get('serialPort').hasError('max')) && |
||||
|
slaveConfigFormGroup.get('serialPort').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate> |
||||
|
gateway.method |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="method"> |
||||
|
<mat-option *ngFor="let method of modbusMethodTypes" [value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.unit-id-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') && |
||||
|
slaveConfigFormGroup.get('unitId').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.device-name</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-name-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') && |
||||
|
slaveConfigFormGroup.get('deviceName').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-profile-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') && |
||||
|
slaveConfigFormGroup.get('deviceType').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.poll-period</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<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 }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="this.slaveConfigFormGroup.get('type').value === ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.baudrate</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="baudrate" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</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> |
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="slaveConfigFormGroup"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.byte-order</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="byteOrder"> |
||||
|
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<mat-expansion-panel *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial" |
||||
|
[expanded]="showSecurityControl.value"> |
||||
|
<mat-expansion-panel-header class="nested-expansion-header"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.tls-connection' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<tb-modbus-security-config formControlName="security" [isMaster]="true"></tb-modbus-security-config> |
||||
|
</mat-expansion-panel> |
||||
|
<ng-container [formGroup]="slaveConfigFormGroup.get('identity')"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.vendor-name</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="vendorName" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.product-code</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="productCode" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.vendor-url</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="vendorUrl" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.product-name</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="productName" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.model-name</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="modelName" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</mat-expansion-panel> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.values</div> |
||||
|
<tb-modbus-values formControlName="values"></tb-modbus-values> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,229 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators, |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ModbusMethodLabelsMap, |
||||
|
ModbusMethodType, |
||||
|
ModbusOrderType, |
||||
|
ModbusProtocolLabelsMap, |
||||
|
ModbusProtocolType, |
||||
|
ModbusRegisterValues, |
||||
|
ModbusSlave, |
||||
|
noLeadTrailSpacesRegex, |
||||
|
PortLimits, |
||||
|
SlaveConfig, |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { GatewayPortTooltipPipe } from '@home/pipes/public-api'; |
||||
|
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; |
||||
|
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-server-config', |
||||
|
templateUrl: './modbus-slave-config.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ModbusSlaveConfigComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => ModbusSlaveConfigComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
standalone: true, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
ModbusValuesComponent, |
||||
|
ModbusSecurityConfigComponent, |
||||
|
GatewayPortTooltipPipe, |
||||
|
], |
||||
|
styles: [` |
||||
|
:host { |
||||
|
.nested-expansion-header { |
||||
|
.mat-content { |
||||
|
height: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
`],
|
||||
|
}) |
||||
|
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { |
||||
|
|
||||
|
slaveConfigFormGroup: UntypedFormGroup; |
||||
|
showSecurityControl: UntypedFormControl; |
||||
|
ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; |
||||
|
ModbusMethodLabelsMap = ModbusMethodLabelsMap; |
||||
|
portLimits = PortLimits; |
||||
|
|
||||
|
readonly modbusProtocolTypes = Object.values(ModbusProtocolType); |
||||
|
readonly modbusMethodTypes = Object.values(ModbusMethodType); |
||||
|
readonly modbusOrderType = Object.values(ModbusOrderType); |
||||
|
readonly ModbusProtocolType = ModbusProtocolType; |
||||
|
readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; |
||||
|
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; |
||||
|
|
||||
|
private onChange: (value: SlaveConfig) => void; |
||||
|
private onTouched: () => void; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
this.showSecurityControl = this.fb.control(false); |
||||
|
this.slaveConfigFormGroup = this.fb.group({ |
||||
|
type: [ModbusProtocolType.TCP, []], |
||||
|
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], |
||||
|
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
method: [ModbusMethodType.SOCKET, []], |
||||
|
unitId: [null, [Validators.required]], |
||||
|
baudrate: [null, []], |
||||
|
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
pollPeriod: [null, []], |
||||
|
sendDataToThingsBoard: [false, []], |
||||
|
byteOrder:[ModbusOrderType.BIG, []], |
||||
|
security: [], |
||||
|
identity: this.fb.group({ |
||||
|
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
productCode: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
vendorUrl: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
productName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
modelName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
}), |
||||
|
values: [], |
||||
|
}); |
||||
|
|
||||
|
this.slaveConfigFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((value: SlaveConfig) => { |
||||
|
if (value.type === ModbusProtocolType.Serial) { |
||||
|
value.port = value.serialPort; |
||||
|
delete value.serialPort; |
||||
|
} |
||||
|
this.onChange(value); |
||||
|
this.onTouched(); |
||||
|
}); |
||||
|
|
||||
|
this.observeTypeChange(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: SlaveConfig) => void): void { |
||||
|
this.onChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: () => void): void { |
||||
|
this.onTouched = fn; |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.slaveConfigFormGroup.valid ? null : { |
||||
|
serverConfigFormGroup: { valid: false } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(slaveConfig: ModbusSlave): void { |
||||
|
this.showSecurityControl.patchValue(!!slaveConfig.security); |
||||
|
this.updateSlaveConfig(slaveConfig); |
||||
|
this.updateControlsEnabling(slaveConfig.type); |
||||
|
} |
||||
|
|
||||
|
private observeTypeChange(): void { |
||||
|
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => { |
||||
|
this.updateControlsEnabling(type); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateControlsEnabling(type: ModbusProtocolType): void { |
||||
|
if (type === ModbusProtocolType.Serial) { |
||||
|
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); |
||||
|
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); |
||||
|
} else { |
||||
|
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); |
||||
|
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private updateSlaveConfig(slaveConfig: ModbusSlave): void { |
||||
|
const { |
||||
|
type, |
||||
|
method, |
||||
|
unitId, |
||||
|
deviceName, |
||||
|
deviceType, |
||||
|
pollPeriod, |
||||
|
sendDataToThingsBoard, |
||||
|
byteOrder, |
||||
|
security, |
||||
|
identity, |
||||
|
values, |
||||
|
baudrate, |
||||
|
host, |
||||
|
port, |
||||
|
} = slaveConfig; |
||||
|
let slaveState: ModbusSlave = { |
||||
|
host: host ?? '', |
||||
|
type: type ?? ModbusProtocolType.TCP, |
||||
|
method: method ?? ModbusMethodType.SOCKET, |
||||
|
unitId: unitId ?? null, |
||||
|
deviceName: deviceName ?? '', |
||||
|
deviceType: deviceType ?? '', |
||||
|
pollPeriod: pollPeriod ?? null, |
||||
|
sendDataToThingsBoard: !!sendDataToThingsBoard, |
||||
|
byteOrder: byteOrder ?? ModbusOrderType.BIG, |
||||
|
security: security ?? {}, |
||||
|
identity: identity ?? { |
||||
|
vendorName: '', |
||||
|
productCode: '', |
||||
|
vendorUrl: '', |
||||
|
productName: '', |
||||
|
modelName: '', |
||||
|
}, |
||||
|
values: values ?? {} as ModbusRegisterValues, |
||||
|
port: port ?? null, |
||||
|
}; |
||||
|
if (slaveConfig.type === ModbusProtocolType.Serial) { |
||||
|
slaveState = { ...slaveState, baudrate, serialPort: port, host: '', port: null } as ModbusSlave; |
||||
|
} else { |
||||
|
slaveState = { ...slaveState, serialPort: '', baudrate: null } as ModbusSlave; |
||||
|
} |
||||
|
this.slaveConfigFormGroup.setValue(slaveState, {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,354 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="slaves-config-container"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ 'gateway.server-slave' | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<div [tb-help]="modbusHelpLink"></div> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.name</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.name-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('name').hasError('required') && |
||||
|
slaveConfigFormGroup.get('name').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="stroked tb-form-panel"> |
||||
|
<div class="tb-form-panel no-border no-padding padding-top"> |
||||
|
<div class="tb-flex row space-between align-center no-gap fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.server-connection</div> |
||||
|
<tb-toggle-select formControlName="type" appearance="fill"> |
||||
|
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-border no-padding padding-top"> |
||||
|
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial" |
||||
|
class="tb-form-row column-xs" |
||||
|
fxLayoutAlign="space-between center" |
||||
|
> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.host</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.host-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('host').hasError('required') |
||||
|
&& slaveConfigFormGroup.get('host').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial else serialPort" |
||||
|
class="tb-form-row column-xs" |
||||
|
fxLayoutAlign="space-between center" |
||||
|
> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}" |
||||
|
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip" |
||||
|
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') || |
||||
|
slaveConfigFormGroup.get('port').hasError('min') || |
||||
|
slaveConfigFormGroup.get('port').hasError('max')) && |
||||
|
slaveConfigFormGroup.get('port').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #serialPort> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.port</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip" |
||||
|
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') || |
||||
|
slaveConfigFormGroup.get('serialPort').hasError('min') || |
||||
|
slaveConfigFormGroup.get('serialPort').hasError('max')) && |
||||
|
slaveConfigFormGroup.get('serialPort').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate> |
||||
|
gateway.method |
||||
|
</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="method"> |
||||
|
<mat-option *ngFor="let method of modbusMethodTypes" [value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-container *ngIf="this.slaveConfigFormGroup.get('type').value === ModbusProtocolType.Serial"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.baudrate</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="baudrate" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.bytesize</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="bytesize" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.stopbits</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="stopbits" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.parity</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="parity" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="strict"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.strict' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.unit-id-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') && |
||||
|
slaveConfigFormGroup.get('unitId').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.device-name</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-name-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') && |
||||
|
slaveConfigFormGroup.get('deviceName').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.device-profile-required') | translate" |
||||
|
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') && |
||||
|
slaveConfigFormGroup.get('deviceType').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.send-data-on-change' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<div class="tb-form-panel no-border no-padding padding-top"> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.connection-timeout</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" name="value" formControlName="timeout" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.byte-order</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="byteOrder"> |
||||
|
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.word-order</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="wordOrder"> |
||||
|
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<mat-expansion-panel *ngIf="this.slaveConfigFormGroup.get('security')" [expanded]="showSecurityControl.value"> |
||||
|
<mat-expansion-panel-header class="nested-expansion-header"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.tls-connection' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config> |
||||
|
</mat-expansion-panel> |
||||
|
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="retries"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.retries' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="retryOnEmpty"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.retries-on-empty' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row" fxLayoutAlign="space-between center"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="retryOnInvalid"> |
||||
|
<mat-label> |
||||
|
{{ 'gateway.retries-on-invalid' | translate }} |
||||
|
</mat-label> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.poll-period</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<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 }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.connect-attempt-time</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<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 }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.connect-attempt-count</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<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 }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> |
||||
|
<div class="fixed-title-width" translate>gateway.wait-after-failed-attempts</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<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 }}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</mat-expansion-panel> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>gateway.values</div> |
||||
|
<tb-modbus-values [singleMode]="true" formControlName="values"></tb-modbus-values> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
cdkFocusInitial |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
(click)="add()" |
||||
|
[disabled]="slaveConfigFormGroup.invalid || !slaveConfigFormGroup.dirty"> |
||||
|
{{ this.data.buttonTitle | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,196 @@ |
|||||
|
///
|
||||
|
/// 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, Inject, OnDestroy } from '@angular/core'; |
||||
|
import { |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validators, |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
MappingInfo, |
||||
|
ModbusMethodLabelsMap, |
||||
|
ModbusMethodType, |
||||
|
ModbusOrderType, |
||||
|
ModbusProtocolLabelsMap, |
||||
|
ModbusProtocolType, |
||||
|
noLeadTrailSpacesRegex, |
||||
|
PortLimits, SlaveConfig, |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { ModbusValuesComponent } from '../modbus-values/modbus-values.component'; |
||||
|
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-slave-dialog', |
||||
|
templateUrl: './modbus-slave-dialog.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ModbusSlaveDialogComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => ModbusSlaveDialogComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
standalone: true, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
ModbusValuesComponent, |
||||
|
ModbusSecurityConfigComponent, |
||||
|
GatewayPortTooltipPipe, |
||||
|
], |
||||
|
styles: [` |
||||
|
:host { |
||||
|
.slaves-config-container { |
||||
|
width: 900px; |
||||
|
} |
||||
|
.nested-expansion-header { |
||||
|
.mat-content { |
||||
|
height: 100%; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
`],
|
||||
|
}) |
||||
|
export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialogComponent, SlaveConfig> implements OnDestroy { |
||||
|
|
||||
|
slaveConfigFormGroup: UntypedFormGroup; |
||||
|
showSecurityControl: UntypedFormControl; |
||||
|
portLimits = PortLimits; |
||||
|
|
||||
|
readonly modbusProtocolTypes = Object.values(ModbusProtocolType); |
||||
|
readonly modbusMethodTypes = Object.values(ModbusMethodType); |
||||
|
readonly modbusOrderType = Object.values(ModbusOrderType); |
||||
|
readonly ModbusProtocolType = ModbusProtocolType; |
||||
|
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; |
||||
|
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap; |
||||
|
readonly modbusHelpLink = |
||||
|
'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters'; |
||||
|
readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict']; |
||||
|
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor( |
||||
|
private fb: FormBuilder, |
||||
|
protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: MappingInfo, |
||||
|
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>, |
||||
|
) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.showSecurityControl = this.fb.control(false); |
||||
|
this.slaveConfigFormGroup = this.fb.group({ |
||||
|
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
type: [ModbusProtocolType.TCP, [Validators.required]], |
||||
|
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], |
||||
|
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
method: [ModbusMethodType.SOCKET, []], |
||||
|
baudrate: [null, []], |
||||
|
stopbits: [null, []], |
||||
|
bytesize: [null, []], |
||||
|
parity: [null, []], |
||||
|
strict: [false, []], |
||||
|
unitId: [null, [Validators.required]], |
||||
|
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
sendDataOnlyOnChange: [false, []], |
||||
|
timeout: [], |
||||
|
byteOrder: [ModbusOrderType.BIG, []], |
||||
|
wordOrder: [ModbusOrderType.BIG, []], |
||||
|
retries: [false, []], |
||||
|
retryOnEmpty: [false, []], |
||||
|
retryOnInvalid: [false, []], |
||||
|
pollPeriod: [null, []], |
||||
|
connectAttemptTimeMs: [null, []], |
||||
|
connectAttemptCount: [null, []], |
||||
|
waitAfterFailedAttemptsMs: [null, []], |
||||
|
values: [{}, []], |
||||
|
security: [{}], |
||||
|
}); |
||||
|
|
||||
|
this.slaveConfigFormGroup.patchValue({ |
||||
|
...this.data.value, |
||||
|
values: { |
||||
|
attributes: this.data.value.attributes ?? [], |
||||
|
timeseries: this.data.value.timeseries ?? [], |
||||
|
attributeUpdates: this.data.value.attributeUpdates ?? [], |
||||
|
rpc: this.data.value.rpc ?? [], |
||||
|
} |
||||
|
}); |
||||
|
this.showSecurityControl.patchValue(!!this.data.value.security); |
||||
|
this.updateControlsEnabling(this.data.value.type); |
||||
|
this.observeTypeChange(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
if (this.slaveConfigFormGroup.valid) { |
||||
|
const slaveResult = {...this.slaveConfigFormGroup.value, ...this.slaveConfigFormGroup.value.values}; |
||||
|
delete slaveResult.values; |
||||
|
if (slaveResult.type === ModbusProtocolType.Serial) { |
||||
|
slaveResult.port = slaveResult.serialPort; |
||||
|
delete slaveResult.serialPort; |
||||
|
} |
||||
|
this.dialogRef.close(slaveResult); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private observeTypeChange(): void { |
||||
|
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => { |
||||
|
this.updateControlsEnabling(type); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateControlsEnabling(type: ModbusProtocolType): void { |
||||
|
if (type === ModbusProtocolType.Serial) { |
||||
|
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); |
||||
|
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); |
||||
|
} else { |
||||
|
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); |
||||
|
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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 *ngIf="singleMode else multipleView"> |
||||
|
<div [formGroup]="valuesFormGroup" class="tb-form-panel no-border no-padding padding-top" fxLayout="column"> |
||||
|
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: null}"></ng-container> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-template #multipleView> |
||||
|
<mat-tab-group [formGroup]="valuesFormGroup"> |
||||
|
<mat-tab *ngFor="let register of modbusRegisterTypes" label="{{ ModbusValuesTranslationsMap.get(register) | translate }}"> |
||||
|
<div [formGroup]="valuesFormGroup.get(register)" class="tb-form-panel no-border no-padding padding-top" fxLayout="column"> |
||||
|
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: register}"></ng-container> |
||||
|
</div> |
||||
|
</mat-tab> |
||||
|
</mat-tab-group> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #singleView let-register> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.attributes</div> |
||||
|
<div class="tb-flex ellipsis-chips-container"> |
||||
|
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES, register)" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let attribute of getValueGroup(ModbusValueKey.ATTRIBUTES, register)"> |
||||
|
{{ attribute.tag }} |
||||
|
</mat-chip> |
||||
|
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
||||
|
<label class="ellipsis-text"></label> |
||||
|
</mat-chip> |
||||
|
</mat-chip-listbox> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
color="primary" |
||||
|
#attributesButton |
||||
|
(click)="manageKeys($event, attributesButton, ModbusValueKey.ATTRIBUTES, register)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.timeseries</div> |
||||
|
<div class="tb-flex ellipsis-chips-container"> |
||||
|
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.TIMESERIES, register)"> |
||||
|
<mat-chip *ngFor="let telemetry of getValueGroup(ModbusValueKey.TIMESERIES, register)"> |
||||
|
{{ telemetry.tag }} |
||||
|
</mat-chip> |
||||
|
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
||||
|
<label class="ellipsis-text"></label> |
||||
|
</mat-chip> |
||||
|
</mat-chip-listbox> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
color="primary" |
||||
|
#telemetryButton |
||||
|
(click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.attribute-updates</div> |
||||
|
<div class="tb-flex ellipsis-chips-container"> |
||||
|
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register)" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let attributeUpdate of getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register)"> |
||||
|
{{ attributeUpdate.tag }} |
||||
|
</mat-chip> |
||||
|
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
||||
|
<label class="ellipsis-text"></label> |
||||
|
</mat-chip> |
||||
|
</mat-chip-listbox> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
color="primary" |
||||
|
#attributesUpdatesButton |
||||
|
(click)="manageKeys($event, attributesUpdatesButton, ModbusValueKey.ATTRIBUTES_UPDATES, register)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex"> |
||||
|
<div class="fixed-title-width" translate>gateway.rpc-requests</div> |
||||
|
<div class="tb-flex ellipsis-chips-container"> |
||||
|
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.RPC_REQUESTS, register)" class="tb-flex"> |
||||
|
<mat-chip *ngFor="let rpcRequest of getValueGroup(ModbusValueKey.RPC_REQUESTS, register)"> |
||||
|
{{ rpcRequest.tag }} |
||||
|
</mat-chip> |
||||
|
<mat-chip class="mat-mdc-chip ellipsis-chip"> |
||||
|
<label class="ellipsis-text"></label> |
||||
|
</mat-chip> |
||||
|
</mat-chip-listbox> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
color="primary" |
||||
|
#rpcRequestsButton |
||||
|
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)"> |
||||
|
<tb-icon matButtonIcon>edit</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
|
||||
@ -0,0 +1,204 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnDestroy, |
||||
|
Renderer2, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
FormGroup, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ModbusKeysAddKeyTranslationsMap, |
||||
|
ModbusKeysDeleteKeyTranslationsMap, |
||||
|
ModbusKeysNoKeysTextTranslationsMap, |
||||
|
ModbusKeysPanelTitleTranslationsMap, |
||||
|
ModbusRegisterTranslationsMap, |
||||
|
ModbusRegisterType, |
||||
|
ModbusRegisterValues, |
||||
|
ModbusValueKey, |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { EllipsisChipListDirective } from '@shared/directives/public-api'; |
||||
|
import { MatButton } from '@angular/material/button'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-modbus-values', |
||||
|
templateUrl: './modbus-values.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => ModbusValuesComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => ModbusValuesComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
standalone: true, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
EllipsisChipListDirective, |
||||
|
], |
||||
|
styles: [` |
||||
|
:host { |
||||
|
.mat-mdc-tab-body-wrapper { |
||||
|
min-height: 320px; |
||||
|
} |
||||
|
} |
||||
|
`]
|
||||
|
}) |
||||
|
|
||||
|
export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { |
||||
|
|
||||
|
@Input() singleMode = false; |
||||
|
|
||||
|
modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType); |
||||
|
modbusValueKeys = Object.values(ModbusValueKey); |
||||
|
ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap; |
||||
|
ModbusValueKey = ModbusValueKey; |
||||
|
valuesFormGroup: FormGroup; |
||||
|
|
||||
|
private onChange: (value: string) => void; |
||||
|
private onTouched: () => void; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private popoverService: TbPopoverService, |
||||
|
private renderer: Renderer2, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private cdr: ChangeDetectorRef, |
||||
|
) { |
||||
|
this.valuesFormGroup = this.fb.group(this.modbusRegisterTypes.reduce((registersAcc, register) => { |
||||
|
return { |
||||
|
...registersAcc, |
||||
|
[register]: this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})), |
||||
|
}; |
||||
|
}, {})); |
||||
|
|
||||
|
this.observeValuesChanges(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(): void { |
||||
|
if (this.singleMode) { |
||||
|
this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})); |
||||
|
this.observeValuesChanges(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (value: string) => void): void { |
||||
|
this.onChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: () => void): void { |
||||
|
this.onTouched = fn; |
||||
|
} |
||||
|
|
||||
|
writeValue(values: ModbusRegisterValues): void { |
||||
|
this.valuesFormGroup.patchValue(values, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.valuesFormGroup.valid ? null : { |
||||
|
valuesFormGroup: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType) { |
||||
|
return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value; |
||||
|
} |
||||
|
|
||||
|
manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const trigger = matButton._elementRef.nativeElement; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
} else { |
||||
|
const group = this.valuesFormGroup; |
||||
|
|
||||
|
const keysControl = register ? group.get(register).get(keysType) : group.get(keysType); |
||||
|
const ctx = { |
||||
|
values: keysControl.value, |
||||
|
isMaster: !this.singleMode, |
||||
|
keysType, |
||||
|
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType), |
||||
|
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType), |
||||
|
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType), |
||||
|
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType) |
||||
|
}; |
||||
|
const dataKeysPanelPopover = this.popoverService.displayPopover( |
||||
|
trigger, |
||||
|
this.renderer, |
||||
|
this.viewContainerRef, |
||||
|
ModbusDataKeysPanelComponent, |
||||
|
'leftBottom', |
||||
|
false, |
||||
|
null, |
||||
|
ctx, |
||||
|
{}, |
||||
|
{}, |
||||
|
{}, |
||||
|
true |
||||
|
); |
||||
|
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; |
||||
|
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => { |
||||
|
dataKeysPanelPopover.hide(); |
||||
|
keysControl.patchValue(keysData); |
||||
|
keysControl.markAsDirty(); |
||||
|
this.cdr.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private observeValuesChanges(): void { |
||||
|
this.valuesFormGroup.valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(value => { |
||||
|
this.onChange(value); |
||||
|
this.onTouched(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
///
|
||||
|
/// 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 './modbus-basic-config/modbus-basic-config.component'; |
||||
|
export * from './modbus-values/modbus-values.component'; |
||||
|
export * from './modbus-data-keys-panel/modbus-data-keys-panel.component'; |
||||
|
export * from './modbus-slave-config/modbus-slave-config.component'; |
||||
|
export * from './modbus-master-table/modbus-master-table.component'; |
||||
|
export * from './modbus-slave-dialog/modbus-slave-dialog.component'; |
||||
|
export * from './modbus-security-config/modbus-security-config.component'; |
||||
@ -0,0 +1,42 @@ |
|||||
|
///
|
||||
|
/// 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 { PortLimits, } from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { AbstractControl } from '@angular/forms'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
@Pipe({ |
||||
|
name: 'getGatewayPortTooltip', |
||||
|
standalone: true, |
||||
|
}) |
||||
|
export class GatewayPortTooltipPipe implements PipeTransform { |
||||
|
|
||||
|
constructor(private translate: TranslateService) {} |
||||
|
|
||||
|
transform(portControl: AbstractControl): string { |
||||
|
if (portControl.hasError('required')) { |
||||
|
return this.translate.instant('gateway.port-required'); |
||||
|
} else if ( |
||||
|
portControl.hasError('min') || |
||||
|
portControl.hasError('max') |
||||
|
) { |
||||
|
return this.translate.instant('gateway.port-limits-error', |
||||
|
{min: PortLimits.MIN, max: PortLimits.MAX}); |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
///
|
||||
|
/// 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 { DataSource } from '@angular/cdk/collections'; |
||||
|
import { BehaviorSubject, Observable } from 'rxjs'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
export abstract class TbDatasource<T> implements DataSource<T> { |
||||
|
|
||||
|
protected dataSubject = new BehaviorSubject<Array<T>>([]); |
||||
|
|
||||
|
connect(): Observable<Array<T>> { |
||||
|
return this.dataSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(): void { |
||||
|
this.dataSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
loadData(data: Array<T>): void { |
||||
|
this.dataSubject.next(data); |
||||
|
} |
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.dataSubject.pipe( |
||||
|
map((data: T[]) => !data.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.dataSubject.pipe( |
||||
|
map((data: T[]) => data.length) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
///
|
||||
|
/// 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 './datasource/datasource.abstract'; |
||||
Loading…
Reference in new issue