Browse Source

lwm2m: bootstrap new: add reboot device and reboot device bootstrap

pull/14084/head
nickAS21 10 months ago
parent
commit
c593f3e95b
  1. 25
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java
  2. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
  3. 6
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  4. 14
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html
  5. 128
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts
  6. 3
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  7. 4
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  8. 4
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html
  9. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts
  10. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

25
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java

@ -47,11 +47,11 @@ public enum Lwm2mServerIdentifier {
*/
NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX(65535, "Reserved sentinel value (no active server)", false);
private final int id;
private final Integer id;
private final String description;
private final boolean isLwm2mServer;
Lwm2mServerIdentifier(int id, String description, boolean isLwm2mServer) {
Lwm2mServerIdentifier(Integer id, String description, boolean isLwm2mServer) {
this.id = id;
this.description = description;
this.isLwm2mServer = isLwm2mServer;
@ -60,7 +60,7 @@ public enum Lwm2mServerIdentifier {
/**
* @return the integer value of this Short Server ID.
*/
public int getId() {
public Integer getId() {
return id;
}
@ -83,20 +83,11 @@ public enum Lwm2mServerIdentifier {
* @param id Short Server ID value.
* @return true if the ID belongs to a standard LwM2M Server.
*/
public static boolean isLwm2mServer(int id) {
return id >= PRIMARY_LWM2M_SERVER.id && id <= LWM2M_SERVER_MAX.id;
public static boolean isLwm2mServer(Integer id) {
return id != null && id >= PRIMARY_LWM2M_SERVER.id && id <= LWM2M_SERVER_MAX.id;
}
public static boolean isNotLwm2mServer(int id) {
return id < PRIMARY_LWM2M_SERVER.id || id > LWM2M_SERVER_MAX.id;
}
/**
* Checks whether the provided ID is within the valid LwM2M range [065535].
* @param id ID to check.
* @return true if valid, false otherwise.
*/
public static boolean isValid(int id) {
return id >= NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN.getId() && id <= NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId();
public static boolean isNotLwm2mServer(Integer id) {
return id == null || id < PRIMARY_LWM2M_SERVER.id || id > LWM2M_SERVER_MAX.id;
}
/**
@ -105,7 +96,7 @@ public enum Lwm2mServerIdentifier {
* @return corresponding enum constant.
* @throws IllegalArgumentException if no constant matches the given ID.
*/
public static Lwm2mServerIdentifier fromId(int id) {
public static Lwm2mServerIdentifier fromId(Integer id) {
for (Lwm2mServerIdentifier s : values()) {
if (s.id == id) {
return s;

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java

@ -147,7 +147,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
log.error("Invalid lwm2mSecurityInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId);
}
} else {
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(0, path.getObjectInstanceId());
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(null, path.getObjectInstanceId());
}
} else if (path.getObjectId() == 1) {
if (link.getAttributes().get("ssid") != null) {

6
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -343,7 +343,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
if (serverConfig.isBootstrapServerIs()){
if (serverConfig.getShortServerId() != null) {
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null!");
if (serverConfig.getShortServerId() == 0) {
serverConfig.setShortServerId(null);
} else {
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null!");
}
}
} else {
if (serverConfig.getShortServerId() != null) {

14
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-tab-group [formGroup]="lwm2mConfigFormGroup">
<mat-tab-group [formGroup]="lwm2mConfigFormGroup" dynamicHeight>
<mat-tab label="{{ 'device.lwm2m-security-config.client-tab' | translate }}">
<ng-container formGroupName="client">
<mat-form-field class="mat-block">
@ -72,6 +72,12 @@
</textarea>
<mat-hint translate>device.lwm2m-security-config.client-public-key-hint</mat-hint>
</mat-form-field>
<button *ngIf="deviceId"
mat-raised-button color="primary" type="button"
(click)="rebootDevice(false)"
[disabled]="lwm2mConfigFormGroup.get('client').invalid">
{{ 'device.lwm2m-security-config.client-reboot' | translate }}
</button>
</ng-container>
</mat-tab>
<mat-tab label="{{ 'device.lwm2m-security-config.bootstrap-tab' | translate }}">
@ -103,5 +109,11 @@
</mat-expansion-panel>
</mat-accordion>
</div>
<button *ngIf="deviceId"
mat-raised-button color="primary" style="margin-top: 22px" type="button"
(click)="rebootDevice(true)"
[disabled]="lwm2mConfigFormGroup.get('client').invalid">
{{ 'device.lwm2m-security-config.bootstrap-reboot' | translate }}
</button>
</mat-tab>
</mat-tab-group>

128
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, OnDestroy } from '@angular/core';
import {Component, forwardRef, Input, OnDestroy} from '@angular/core';
import {
ControlValueAccessor,
UntypedFormBuilder,
@ -32,9 +32,12 @@ import {
Lwm2mSecurityType,
Lwm2mSecurityTypeTranslationMap
} from '@shared/models/lwm2m-security-config.models';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {Subject, throwError, timeout, catchError, of} from 'rxjs';
import {map, takeUntil} from 'rxjs/operators';
import { isDefinedAndNotNull } from '@core/utils';
import { HttpClient } from '@angular/common/http';
import {DeviceId} from "@shared/models/id/device-id";
import {Observable} from "rxjs/internal/Observable";
@Component({
selector: 'tb-device-credentials-lwm2m',
@ -65,7 +68,11 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(private fb: UntypedFormBuilder) {
@Input()
deviceId: DeviceId;
constructor(private fb: UntypedFormBuilder,
private http: HttpClient) {
this.lwm2mConfigFormGroup = this.initLwm2mConfigForm();
}
@ -101,6 +108,119 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
this.destroy$.complete();
}
/**
* AbstractRpcController -> rpcController
* - API
* "/api/plugins/rpc/twoway/${this.deviceId.id}"
* - DiscoveryAll
* requestBody = "{\"method\":\"DiscoverAll\"}";
* - "Registration Update Trigger",
* requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1_1.2/0/8\"}}
* - "Bootstrap-Request Trigger"
* requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1_1.2/0/9\"}}
*/
public rebootDevice(isBootstrapServer: boolean): void {
const urlApi = `/api/plugins/rpc/twoway/${this.deviceId.id}`;
// DiscoveryAll}
this.http.post(urlApi, { method: "DiscoverAll" })
.pipe(
timeout(10000), // 10 sec
catchError(err => {
console.error('DiscoverAll timeout or error', err);
return throwError(() => err);
})
)
.subscribe({
next: (response: any) => {
console.log('success: Discovery');
console.log(response);
// result = 'CONTENT'
if (response.result && response.result.toUpperCase() === 'CONTENT') {
const verId = this.getVerId(response.value);
console.log("ObjectId=1 ver:", verId);
const resourceId = isBootstrapServer ? 9 : 8;
const resourcePath = `/1_${verId}/0/${resourceId}`;
// first rebootTrigger
this.rebootTrigger(resourcePath, urlApi).subscribe(first => {
if (first.result === 'CHANGED') {
console.log('Reboot success first');
}
else if (first.result === 'BAD_REQUEST' && first.newVersionId && first.newVersionId !== verId) {
// Retry with new version
const correctedPath = `/1_${first.newVersionId}/0/${resourceId}`;
console.log(`Retrying with version ${first.newVersionId}`);
this.rebootTrigger(correctedPath, urlApi).subscribe(second => {
if (second.result === 'CHANGED') {
console.log('Success reboot after retry');
} else {
console.error(`error1: Reboot second failed: ${second.toString()}`);
}
});
} else {
console.error(`error2: Reboot first failed: ${first.toString()}`);
}
});
}
else {
console.error(`error3: Bad registration device with id = ${this.deviceId.id} ❗ RPC result is not CONTENT`);
}
},
error: (e) => {
console.error(`error4: Bad registration device with id = ${this.deviceId.id} ${e.toString()}`);
return throwError(() => e);
},
complete: () => {
console.log('Discovery stream complete'); }
});
}
private getVerId(value: string): string {
const verDef = '1.1';
try {
const arr = JSON.parse(value);
if (!Array.isArray(arr)) return verDef;
const obj1 = arr.find((s: string) => s.startsWith('</1'));
const match = obj1?.match(/ver="?([\d.]+)"?/);
return match ? match[1] : verDef;
} catch {
return verDef;
}
}
private rebootTrigger(resourcePath: string, urlApi: string): Observable<{ result: string; newVersionId?: string | null }> {
console.log(`Sending reboot command to ${resourcePath}`);
return this.http.post<any>(urlApi, {
method: 'Execute',
params: { id: resourcePath }
}).pipe(
timeout(10000),
map(res => {
console.log(`Reboot for ${resourcePath}`);
console.log(res);
if (res?.result?.toUpperCase() === 'CHANGED') {
return { result: 'CHANGED' };
}
if (res?.result?.toUpperCase() === 'BAD_REQUEST' && res?.error) {
const match = (res.error as string).match(/version[:=]\s*([\d.]+)/i);
const newVersionId = match ? match[1] : null;
console.warn(`BAD_REQUEST: suggested version ${newVersionId ?? 'unknown'}`);
return { result: 'BAD_REQUEST', newVersionId };
}
return { result: 'ERROR' };
}),
catchError(err => {
console.error(`Execute error5 for ${resourcePath}:`, err);
return of({ result: 'ERROR' });
})
);
}
private initClientSecurityConfig(config: Lwm2mSecurityConfigModels): void {
this.lwm2mConfigFormGroup.patchValue(config, {emitEvent: false});
this.securityConfigClientUpdateValidators(config.client.securityConfigClientMode);

3
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -81,7 +81,8 @@
</tb-device-credentials-mqtt-basic>
</ng-template>
<ng-template [ngSwitchCase]="deviceCredentialsType.LWM2M_CREDENTIALS">
<tb-device-credentials-lwm2m formControlName="credentialsValue">
<tb-device-credentials-lwm2m formControlName="credentialsValue"
[deviceId]="deviceId">
</tb-device-credentials-lwm2m>
</ng-template>
</div>

4
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -36,6 +36,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { generateSecret, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import {DeviceId} from "@shared/models/id/device-id";
@Component({
selector: 'tb-device-credentials',
@ -88,6 +89,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
credentialTypeNamesMap = credentialTypeNames;
deviceId: DeviceId;
private propagateChange = null;
private propagateChangePending = false;
@ -126,6 +129,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
writeValue(value: DeviceCredentials | null): void {
if (isDefinedAndNotNull(value)) {
this.deviceId = value.deviceId;
const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0];
this.deviceCredentialsFormGroup.patchValue({
credentialsType,

4
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html

@ -24,7 +24,7 @@
'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}</div>
<div *ngIf="!serverPanel.expanded" style="font-size:14px" class="no-wrap flex flex-row">
<div style="margin-left:32px">{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }}
<span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value }}</span>
<span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value ? serverFormGroup.get('shortServerId').value : '' }}</span>
</div>
<div style="margin-left:32px">{{ ('device-profile.lwm2m.mode' | translate) + ': ' }}
<span style="font-style: italic">{{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }}</span>
@ -54,7 +54,7 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-form-field class="flex-1" *ngIf="!isBootstrap">
<mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label>
<mat-icon *ngIf="!disabled" class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{ (isBootstrap ? 'device-profile.lwm2m.short-id-tooltip-bootstrap': 'device-profile.lwm2m.short-id-tooltip') | translate }}">help</mat-icon>

6
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts

@ -76,7 +76,6 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
readonly shortServerIdMin = 1;
readonly shortServerIdMax = 65534;
readonly shortServerIdBs = 0;
@Input()
@coerceBoolean()
@ -101,9 +100,8 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
securityMode: [Lwm2mSecurityType.NO_SEC],
serverPublicKey: [''],
clientHoldOffTime: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
shortServerId: ['', this.isBootstrap
? [Validators.required, Validators.pattern('^(' + this.shortServerIdBs + ')$' )]
: [Validators.required, Validators.pattern('[0-9]*'), Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax)]
shortServerId: ['', this.isBootstrap ?
[] : [Validators.required, Validators.pattern('[0-9]*'), Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax)]
],
bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
binding: [''],

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

@ -1801,6 +1801,8 @@
"bootstrap-tab": "Bootstrap Client",
"bootstrap-server": "Bootstrap Server",
"lwm2m-server": "LwM2M Server",
"client-reboot": "Registration Update Trigger",
"bootstrap-reboot": "Bootstrap-Request Trigger",
"client-publicKey-or-id": "Client Public Key or Id",
"client-publicKey-or-id-required": "Client Public Key or Id is required.",
"client-publicKey-or-id-tooltip-psk": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.",
@ -2302,7 +2304,7 @@
"short-id-required": "Short server ID is required.",
"short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.",
"short-id-pattern": "Short server ID must be a positive integer.",
"short-id-pattern-bs": "Short server ID must be only 0",
"short-id-pattern-bs": "Short server ID must be only null",
"lifetime": "Client registration lifetime",
"lifetime-required": "Client registration lifetime is required.",
"lifetime-pattern": "Client registration lifetime must be a positive integer.",

Loading…
Cancel
Save