Merge pull request #12799 from thingsboard/feature/lwm2m/obj-19-ota-update

Use LwM2M Object 19 to send OTA update metadata
This commit is contained in:
Andrew Shvayka 2025-04-24 16:11:35 +03:00 committed by GitHub
commit 300cdbe0f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 469 additions and 74 deletions

View File

@ -257,7 +257,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
transportConfiguration.setBootstrap(Collections.emptyList());
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString()));
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString()));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()));
DeviceProfileData deviceProfileData = new DeviceProfileData();

View File

@ -102,6 +102,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClient
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19;
@TestPropertySource(properties = {
"transport.lwm2m.enabled=true",
@ -383,6 +384,17 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return transportConfiguration;
}
protected Lwm2mDeviceProfileTransportConfiguration getTransportConfiguration19(String observeAttr, List<LwM2MBootstrapServerCredential> bootstrapServerCredentials) {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
TelemetryMappingConfiguration observeAttrConfiguration = JacksonUtil.fromString(observeAttr, TelemetryMappingConfiguration.class);
OtherConfiguration clientLwM2mSettings = JacksonUtil.fromString(CLIENT_LWM2M_SETTINGS_19, OtherConfiguration.class);
transportConfiguration.setBootstrapServerUpdateEnable(true);
transportConfiguration.setObserveAttr(observeAttrConfiguration);
transportConfiguration.setClientLwM2mSettings(clientLwM2mSettings);
transportConfiguration.setBootstrap(bootstrapServerCredentials);
return transportConfiguration;
}
protected List<LwM2MBootstrapServerCredential> getBootstrapServerCredentialsNoSec(LwM2MProfileBootstrapConfigType bootstrapConfigType) {
List<LwM2MBootstrapServerCredential> bootstrap = new ArrayList<>();
switch (bootstrapConfigType) {

View File

@ -16,10 +16,14 @@
package org.thingsboard.server.transport.lwm2m.ota;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.dockerjava.zerodep.shaded.org.apache.commons.codec.binary.Hex;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.ResponseCode;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -31,25 +35,30 @@ import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.*;
@Slf4j
@DaoSqlTest
public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
private final String[] RESOURCES_OTA = new String[]{"3.xml", "5.xml", "9.xml"};
private final String[] RESOURCES_OTA = new String[]{"3.xml", "5.xml", "9.xml", "19.xml"};
protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice";
protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device";
protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device";
protected static final String CLIENT_ENDPOINT_OTA9_19 = "Ota9_Device_19";
protected List<OtaPackageUpdateStatus> expectedStatuses;
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5 =
" {\n" +
@ -78,6 +87,51 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"attributeLwm2m\": {}\n" +
" }";
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5_19 =
" {\n" +
" \"keyName\": {\n" +
" \"/5_1.2/0/3\": \"state\",\n" +
" \"/5_1.2/0/5\": \"updateResult\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/5_1.2/0/7\": \"pkgversion\",\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\",\n" +
" \"/19_1.1/0/0\": \"dataRead\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/19_1.1/0/0\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" +
" \"/19_1.1/0/0\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
public static final String CLIENT_LWM2M_SETTINGS_19 =
" {\n" +
" \"useObject19ForOta\": true,\n" +
" \"edrxCycle\": null,\n" +
" \"powerMode\": \"DRX\",\n" +
" \"fwUpdateResource\": null,\n" +
" \"fwUpdateStrategy\": 1,\n" +
" \"psmActivityTimer\": null,\n" +
" \"swUpdateResource\": null,\n" +
" \"swUpdateStrategy\": 1,\n" +
" \"pagingTransmissionWindow\": null,\n" +
" \"clientOnlyObserveAfterConnect\": 1\n" +
" }";
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9 =
" {\n" +
@ -102,6 +156,33 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9_19 =
" {\n" +
" \"keyName\": {\n" +
" \"/9_1.1/0/0\": \"pkgname\",\n" +
" \"/9_1.1/0/1\": \"pkgversion\",\n" +
" \"/9_1.1/0/7\": \"updateState\",\n" +
" \"/9_1.1/0/9\": \"updateResult\",\n" +
" \"/19_1.1/0/0\": \"dataRead\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\",\n" +
" \"/19_1.1/0/0\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\",\n" +
" \"/19_1.1/0/0\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
public AbstractOtaLwM2MIntegrationTest() {
setResources(this.RESOURCES_OTA);
@ -123,14 +204,14 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
return savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, "SHA256");
}
protected OtaPackageInfo createSoftware(DeviceProfileId deviceProfileId) throws Exception {
protected OtaPackageInfo createSoftware(DeviceProfileId deviceProfileId, String version) throws Exception {
String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
OtaPackageInfo swInfo = new OtaPackageInfo();
swInfo.setDeviceProfileId(deviceProfileId);
swInfo.setType(SOFTWARE);
swInfo.setTitle("My sw");
swInfo.setVersion("v1.0");
swInfo.setVersion(version);
OtaPackageInfo savedFirmwareInfo = doPost("/api/otaPackage", swInfo, OtaPackageInfo.class);
@ -169,4 +250,27 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
log.warn("{}", statuses);
return statuses.containsAll(expectedStatuses);
}
protected void resultReadOtaParams_19(String resourceIdVer, OtaPackageInfo otaPackageInfo) throws Exception {
String actualResult = sendRPCById(resourceIdVer);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String valStr = rpcActualResult.get("value").asText();
String start = "{ id=0 value=";
String valHexDec = valStr.substring(valStr.indexOf(start) + start.length(), (valStr.indexOf("}")));
String valNode = new String(Hex.decodeHex((valHexDec).toCharArray()));
ObjectNode actualResultVal = JacksonUtil.fromString(valNode, ObjectNode.class);
assert actualResultVal != null;
assertEquals(otaPackageInfo.getTitle(), actualResultVal.get(OTA_INFO_19_TITLE).asText());
assertEquals(otaPackageInfo.getVersion(), actualResultVal.get(OTA_INFO_19_VERSION).asText());
assertEquals(otaPackageInfo.getChecksum(), actualResultVal.get(OTA_INFO_19_FILE_CHECKSUM256).asText());
assertEquals(otaPackageInfo.getFileName(), actualResultVal.get(OTA_INFO_19_FILE_NAME).asText());
assertEquals(Optional.of(otaPackageInfo.getDataSize()), Optional.of((long) actualResultVal.get(OTA_INFO_19_FILE_SIZE).asInt()));
}
private String sendRPCById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
}

View File

@ -21,6 +21,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.kv.KvEntry;
@ -44,7 +45,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INIT
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_INSTANCE_ID;
@Slf4j
public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
@ -107,4 +111,49 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
}
/**
* ObjectId = 19/65533/0
* {
* "title" : "My firmware",
* "version" : "fw.v.1.5.0-update",
* "checksum" : "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a",
* "fileSize" : 1,
* "fileName" : "filename.txt"
* }
* to base64
* /5/0/5 -> Update Result (Res); 5/0/3 -> State;
* => ((Res>=0 && Res<=9) && State=0)
* => Write to Package/Write to Package URI -> DOWNLOADING ((Res>=0 && Res<=9) && State=1)
* => Download Finished -> DOWNLOADED ((Res==0 || Res=8) && State=2)
* => Executable resource Update is triggered / Initiate Firmware Update -> UPDATING (Res=0 && State=3)
* => Update Successful [Res==1]
* => Start / Res=0 -> "IDLE" ....
* @throws Exception
*/
@Test
public void testFirmwareUpdateByObject5WithObject19_Ok() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration19(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5_19, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5));
final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA5, device.getId().getId().toString());
awaitObserveReadAll(6, device.getId().getId().toString());
OtaPackageInfo otaPackageInfo = createFirmware("fw.v.1.5.0-update", deviceProfile.getId());
device.setFirmwareId(otaPackageInfo.getId());
final Device savedDevice = doPost("/api/device", device, Device.class);
assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED);
List<TsKvEntry> ts = await("await on timeseries for FW")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "fw_state"), this::predicateForStatuses);
String ver_Id_19 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version;
String resourceIdVer = "/" + BINARY_APP_DATA_CONTAINER + "_" + ver_Id_19 + "/" + FW_INSTANCE_ID + "/" + RESOURCE_ID_0;
resultReadOtaParams_19(resourceIdVer, otaPackageInfo);
log.warn("Object5: Got the ts: {}", ts);
}
}

View File

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -35,7 +36,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INIT
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_INSTANCE_ID;
@Slf4j
public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
@ -50,14 +54,15 @@ public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
* */
@Test
public void testSoftwareUpdateByObject9() throws Exception {
String clientEndpoint = this.CLIENT_ENDPOINT_OTA9;
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA9, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9));
final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA9, device.getId().getId().toString());
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
final Device device = createLwm2mDevice(deviceCredentials, clientEndpoint, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, clientEndpoint, device.getId().getId().toString());
awaitObserveReadAll(4, device.getId().getId().toString());
device.setSoftwareId(createSoftware(deviceProfile.getId()).getId());
device.setSoftwareId(createSoftware(deviceProfile.getId(), "v1.0").getId());
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call
assertThat(savedDevice).as("saved device").isNotNull();
@ -70,4 +75,47 @@ public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "sw_state"), this::predicateForStatuses);
log.warn("Object9: Got the ts: {}", ts);
}
/**
* ObjectId = 19/65534/0
* {
* "title" : "My sw",
* "version" : "v1.0.19",
* "checksum" : "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a",
* "fileSize" : 1,
* "fileName" : "filename.txt"
* }
* => Start -> INITIAL (State=0) -> DOWNLOAD STARTED;
* => PKG / URI Write -> DOWNLOAD STARTED (Res=1 (Downloading) && State=1) -> DOWNLOADED
* => PKG Written -> DOWNLOADED (Res=1 Initial && State=2) -> DELIVERED;
* => PKG integrity verified -> DELIVERED (Res=3 (Successfully Downloaded and package integrity verified) && State=3) -> INSTALLED;
* => Install -> INSTALLED (Res=2 SW successfully installed) && State=4) -> Start
*
* */
@Test
public void testSoftwareUpdateByObject9WithObject19_Ok() throws Exception {
String clientEndpoint = this.CLIENT_ENDPOINT_OTA9_19;
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration19(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9_19, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
final Device device = createLwm2mDevice(deviceCredentials, clientEndpoint, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, clientEndpoint, device.getId().getId().toString());
awaitObserveReadAll(5, device.getId().getId().toString());
OtaPackageInfo otaPackageInfo = createSoftware(deviceProfile.getId(), "v1.0.19");
device.setSoftwareId(otaPackageInfo.getId());
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call
assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = List.of(
QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED);
List<TsKvEntry> ts = await("await on timeseries")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "sw_state"), this::predicateForStatuses);
String ver_Id_19 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version;
String resourceIdVer = "/" + BINARY_APP_DATA_CONTAINER + "_" + ver_Id_19 + "/" + SW_INSTANCE_ID + "/" + RESOURCE_ID_0;
resultReadOtaParams_19(resourceIdVer, otaPackageInfo);
log.warn("Object9: Got the ts: {}", ts);
}
}

View File

@ -16,26 +16,22 @@
package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.hash.Hashing;
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.Base64;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_INSTANCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.*;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.*;
public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTest {
@ -77,6 +73,28 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes
String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue + ", type=STRING]";
assertTrue(actualValues.contains(expected));
}
@Test
public void testWriteReplaceValueMultipleResource_Result_CHANGED_Multi_Instance_Resource_must_One_ValueJsonNodeToBase64() throws Exception {
LwM2mObjectEnabler lwM2mObjectEnabler = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnabler(BINARY_APP_DATA_CONTAINER);
if (lwM2mObjectEnabler != null) {
String expectedPath = objectIdVer_19 + "/" + FW_INSTANCE_ID + "/";
String expectedValueStr = getJsonNodeBase64();
String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"" + expectedValueStr + "\"}}";
String actualResult = sendRPCreateById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CREATED.getName(), rpcActualResult.get("result").asText());
actualResult = sendRPCReadById(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
String actualValues = rpcActualResult.get("value").asText();
byte[] expectedValue0 = TbUtils.base64ToBytes(expectedValueStr);
String expectedInstance = "LwM2mObjectInstance [id=" + FW_INSTANCE_ID;
assertTrue(actualValues.contains(expectedInstance));
String expected = "{0=LwM2mMultipleResource [id=0, values={0=LwM2mResourceInstance [id=0, value=" + expectedValue0.length + "Bytes, type=OPAQUE]}";
assertTrue(actualValues.contains(expected));
}
}
@Test
public void testWriteReplaceValueMultipleResource_Result_CHANGED_Multi_Instance_Resource_must_One() throws Exception {
int resourceInstanceId0 = 0;
@ -525,4 +543,33 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes
String setRpcRequest = "{\"method\": \"WriteComposite\", \"params\": {\"nodes\":" + nodes + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
/**
* ObjectId = 19/65533/0
* {
* "title" : "My firmware",
* "version" : "fw.v.1.5.0-update",
* "checksum" : "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a",
* "fileSize" : 1,
* "fileName" : "filename.txt"
* }
*/
private String getJsonNodeBase64() {
ObjectNode objectNodeInfoOta = JacksonUtil.newObjectNode();
byte[] firmwareChunk = new byte[]{1};
String fileChecksumSHA256 = Hashing.sha256().hashBytes(firmwareChunk).toString();
objectNodeInfoOta.put(OTA_INFO_19_TITLE, "My firmware");
objectNodeInfoOta.put(OTA_INFO_19_VERSION, "fw.v.1.5.0-update");
objectNodeInfoOta.put(OTA_INFO_19_FILE_CHECKSUM256, fileChecksumSHA256);
objectNodeInfoOta.put(OTA_INFO_19_FILE_SIZE, firmwareChunk.length);
objectNodeInfoOta.put(OTA_INFO_19_FILE_NAME, "filename.txt");
String objectNodeInfoOtaStr = JacksonUtil.toString(objectNodeInfoOta);
assert objectNodeInfoOtaStr != null;
return Base64.getEncoder().encodeToString(objectNodeInfoOtaStr.getBytes());
}
private String sendRPCreateById(String path, String value) throws Exception {
String setRpcRequest = "{\"method\": \"Create\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}";
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
}

View File

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration;
@JsonIgnoreProperties(ignoreUnknown = true)
public class OtherConfiguration extends PowerSavingConfiguration {
private Boolean useObject19ForOtaInfo;
private Integer fwUpdateStrategy;
private Integer swUpdateStrategy;
private Integer clientOnlyObserveAfterConnect;

View File

@ -113,6 +113,7 @@ import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.SHOR
import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.STEP;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OBJLNK;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
import static org.thingsboard.server.common.transport.util.JsonUtils.isBase64;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.createModelsDefault;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@ -399,7 +400,8 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
try {
Object valueForMultiResource = request.getValue();
if (resultIds.isResourceInstance()) {
String resourceInstance = "{" + resultIds.getResourceInstanceId() + "=" + request.getValue() + "}";
String valueStr = isBase64(request.getValue().toString()) ? "\"" + request.getValue() + "\"" : request.getValue().toString();
String resourceInstance = "{" + resultIds.getResourceInstanceId() + "=" + valueStr + "}";
valueForMultiResource = JsonParser.parseString(resourceInstance);
}
Map<Integer, Object> value = convertMultiResourceValuesFromRpcBody(valueForMultiResource, resourceModelWrite.type, request.getObjectId());

View File

@ -15,16 +15,21 @@
*/
package org.thingsboard.server.transport.lwm2m.server.ota;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.hash.Hashing;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.request.ContentFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration;
@ -37,6 +42,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
import org.thingsboard.server.transport.lwm2m.server.attributes.LwM2MAttributesService;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
@ -46,6 +52,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MExecuteCall
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MExecuteRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteReplaceRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteResponseCallback;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCreateRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCreateResponseCallback;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareDeliveryMethod;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateResult;
@ -56,6 +64,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.software.LwM2MClientSwO
import org.thingsboard.server.transport.lwm2m.server.ota.software.LwM2MSoftwareUpdateStrategy;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState;
import org.thingsboard.server.transport.lwm2m.server.rpc.RpcCreateRequest;
import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MClientOtaInfoStore;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
@ -64,6 +73,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import static org.thingsboard.server.common.data.ota.OtaPackageKey.STATE;
@ -104,10 +114,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
public static final String FW_RESULT_ID = "/5/0/5";
public static final String FW_NAME_ID = "/5/0/6";
public static final String FW_VER_ID = "/5/0/7";
/**
* Quectel@Hi15RM1-HLB_V1.0@BC68JAR01A10,V150R100C20B300SP7,V150R100C20B300SP7@8
* Revision:BC68JAR01A10
*/
public static final String FW_3_VER_ID = "/3/0/3";
public static final String FW_DELIVERY_METHOD = "/5/0/9";
@ -122,6 +129,16 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
public static final String SW_RESULT_ID = "/9/0/9";
public static final String SW_UN_INSTALL_ID = "/9/0/6";
public static final int FW_INSTANCE_ID = 65533;
public static final String FW_INFO_19_INSTANCE_ID = "/19/" + FW_INSTANCE_ID;
public static final int SW_INSTANCE_ID = 65534;
public static final String SW_INFO_19_INSTANCE_ID = "/19/" + SW_INSTANCE_ID;
public static final String OTA_INFO_19_TITLE = "title";
public static final String OTA_INFO_19_VERSION = "version";
public static final String OTA_INFO_19_FILE_CHECKSUM256 = "checksum";
public static final String OTA_INFO_19_FILE_SIZE = "dataSize";
public static final String OTA_INFO_19_FILE_NAME = "fileName";
private final Map<String, LwM2MClientFwOtaInfo> fwStates = new ConcurrentHashMap<>();
private final Map<String, LwM2MClientSwOtaInfo> swStates = new ConcurrentHashMap<>();
@ -134,6 +151,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
private final LwM2MTelemetryLogService logService;
private final LwM2mTransportServerHelper helper;
private final TbLwM2MClientOtaInfoStore otaInfoStore;
private final LwM2mVersionedModelProvider modelProvider;
@Autowired
@Lazy
@ -510,6 +528,10 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
} else {
strategy = info.getDeliveryMethod() == FirmwareDeliveryMethod.PULL.code ? LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL : LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY;
}
Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getProfileId()).getClientLwM2mSettings().getUseObject19ForOtaInfo();
if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){
sendInfoToObject19ForOta(client, FW_INFO_19_INSTANCE_ID, response, otaPackageId);
}
switch (strategy) {
case OBJ_5_BINARY:
startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client), otaPackageId);
@ -532,6 +554,10 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) {
UUID otaPackageId = new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB());
LwM2MSoftwareUpdateStrategy strategy = info.getStrategy();
Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getProfileId()).getClientLwM2mSettings().getUseObject19ForOtaInfo();
if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){
sendInfoToObject19ForOta(client, SW_INFO_19_INSTANCE_ID, response, otaPackageId);
}
switch (strategy) {
case BINARY:
startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client), otaPackageId);
@ -566,21 +592,24 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
private void executeFwUpdate(LwM2mClient client) {
log.trace("[{}] Execute SW [{}]", client.getEndpoint(), FW_EXECUTE_ID);
String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(fwExecuteVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, fwExecuteVerId));
}
private void executeSwInstall(LwM2mClient client) {
log.trace("[{}] Execute SW (install) [{}]", client.getEndpoint(), SW_INSTALL_ID);
String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInstallVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInstallVerId));
}
private void executeSwUninstallForUpdate(LwM2mClient client) {
String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInInstallVerId));
log.trace("[{}] Execute SW (uninstall with params(\"1\") ) [{}]", client.getEndpoint(), SW_UN_INSTALL_ID);
String swUnInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swUnInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swUnInstallVerId));
}
private Optional<String> getAttributeValue(List<TransportProtos.TsKvProto> attrs, String keyName) {
@ -641,6 +670,62 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
helper.sendParametersOnThingsboardTelemetry(result, client.getSession(), client.getKeyTsLatestMap());
}
/**
* send to client: versionedId="/19/65533/0/0, value = FwOtaInfo in bas64 -> format json:
* send to client: versionedId="/19/65534/0/0, value = SwOtaInfo in bas64 -> format json:
* {"title":"BC68JAR01",
* "version":"A10",
* "checksum":"f2a08d4963e981c78f2a99f62d8439af4437a72ea7267a8c01d013c072c01ded",
* "fileSize":59832.
* "fileName" : "BC68JAR01A10_TO_BC68JAR01A09_09.bin" }
* @param client
* @param targetId
* @param response
* @param otaPackageId
*/
private void sendInfoToObject19ForOta(LwM2mClient client, String targetId, TransportProtos.GetOtaPackageResponseMsg response, UUID otaPackageId) {
log.trace("[{}] Current info ota toObject19ForOta [{}]", client.getEndpoint(), targetId);
String targetIdVer = convertObjectIdToVersionedId(targetId, client);
ObjectModel objectModel = client.getObjectModel(targetIdVer, modelProvider);
if (objectModel != null) {
try {
if (client.getRegistration().getSupportedObject().get(19) != null) {
ObjectNode objectNodeInfoOta = JacksonUtil.newObjectNode();
byte[] firmwareChunk = otaPackageDataCache.get(otaPackageId.toString(), 0, 0);
String fileChecksumSHA256 = Hashing.sha256().hashBytes(firmwareChunk).toString();
objectNodeInfoOta.put(OTA_INFO_19_TITLE, response.getTitle());
objectNodeInfoOta.put(OTA_INFO_19_VERSION, response.getVersion());
objectNodeInfoOta.put(OTA_INFO_19_FILE_CHECKSUM256, fileChecksumSHA256);
objectNodeInfoOta.put(OTA_INFO_19_FILE_SIZE, firmwareChunk.length);
objectNodeInfoOta.put(OTA_INFO_19_FILE_NAME, response.getFileName());
String objectNodeInfoOtaStr = JacksonUtil.toString(objectNodeInfoOta);
assert objectNodeInfoOtaStr != null;
String objectNodeInfoOtaBase64 = Base64.getEncoder().encodeToString(objectNodeInfoOtaStr.getBytes());
LwM2mPath pathOtaInstance = new LwM2mPath(targetId);
if (client.getRegistration().getAvailableInstances().contains(pathOtaInstance)) {
String versionId = targetIdVer + "/0/0";
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionId).value(objectNodeInfoOtaBase64).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionId));
} else {
String valueResourcesStr = "{\"" + 0 + "\":{\"0\":\"" + objectNodeInfoOtaBase64 + "\"}}";
String valueStr = "{\"id\":\"" + targetIdVer + "\",\"value\":" + valueResourcesStr + "}";
RpcCreateRequest requestBody = JacksonUtil.fromString(valueStr, RpcCreateRequest.class);
assert requestBody != null;
TbLwM2MCreateRequest.TbLwM2MCreateRequestBuilder builder = TbLwM2MCreateRequest.builder().versionedId(targetIdVer);
builder.value(requestBody.getValue()).nodes(requestBody.getNodes()).timeout(clientContext.getRequestTimeout(client));
downlinkHandler.sendCreateRequest(client, builder.build(), new TbLwM2MCreateResponseCallback(uplinkHandler, logService, client, targetIdVer));
}
} else {
String errorMsg = String.format("[%s], Failed to send Info Ota to objectInstance [%s]. The client does not have object 19.", client.getEndpoint(), targetId);
log.trace(errorMsg);
logService.log(client, errorMsg);
}
} catch (Exception e){
log.error("", e);
}
}
}
private static Optional<OtaPackageUpdateStatus> toOtaPackageUpdateStatus(FirmwareUpdateResult fwUpdateResult) {
switch (fwUpdateResult) {
case INITIAL:

View File

@ -77,7 +77,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> {
} else if (StringUtils.isNotEmpty(targetTag) && targetTag.equals(currentPackageId)) {
return false;
} else if (StringUtils.isNotEmpty(currentVersion3)) {
if (StringUtils.isNotEmpty(targetTag) && currentVersion3.contains(targetTag)) {
if (StringUtils.isNotEmpty(targetTag) && (currentVersion3.contains(targetTag) || targetTag.contains(currentVersion3))) {
return false;
}
return !currentVersion3.contains(targetPackageId);

View File

@ -23,6 +23,9 @@ import lombok.ToString;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MClientOtaInfo;
import static org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateResult.UPDATE_SUCCESSFULLY;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@ -43,6 +46,11 @@ public class LwM2MClientFwOtaInfo extends LwM2MClientOtaInfo<LwM2MFirmwareUpdate
public void update(FirmwareUpdateResult result) {
this.result = result;
if (result.getCode() > UPDATE_SUCCESSFULLY.getCode()) {
failedPackageId = getPackageId(targetName, targetVersion);
}
switch (result) {
case INITIAL:
break;
@ -50,7 +58,6 @@ public class LwM2MClientFwOtaInfo extends LwM2MClientOtaInfo<LwM2MFirmwareUpdate
retryAttempts = 0;
break;
default:
failedPackageId = getPackageId(targetName, targetVersion);
break;
}
}

View File

@ -23,6 +23,8 @@ import lombok.ToString;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MClientOtaInfo;
import static org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult.NOT_ENOUGH_STORAGE;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@ -42,12 +44,17 @@ public class LwM2MClientSwOtaInfo extends LwM2MClientOtaInfo<LwM2MSoftwareUpdate
public void update(SoftwareUpdateResult result) {
this.result = result;
if (result.getCode() >= NOT_ENOUGH_STORAGE.getCode()) {
failedPackageId = getPackageId(targetName, targetVersion);
}
switch (result) {
case INITIAL:
break;
//TODO: implement
case SUCCESSFULLY_INSTALLED:
retryAttempts = 0;
break;
default:
failedPackageId = getPackageId(targetName, targetVersion);
break;
}
}

View File

@ -409,7 +409,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile);
}
} catch (Exception e) {
log.warn("[{}] failed to update profile: {}", deviceProfile.getId(), deviceProfile);
log.warn("[{}] failed to update profile: {} [{}]", deviceProfile.getId(), e.getMessage(), deviceProfile);
}
}
@ -424,7 +424,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
this.onDeviceUpdate(client, device, newDeviceProfileOpt);
}
} catch (Exception e) {
log.warn("[{}] failed to update device: {}", device.getId(), device);
log.warn("[{}] failed to update device: {} [{}]", device.getId(), e.getMessage(), device);
}
}

View File

@ -31,6 +31,7 @@ import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import static org.eclipse.leshan.core.model.ResourceModel.Type.NONE;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
@Slf4j
@ -53,14 +54,15 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter {
return value;
}
if (currentType == expectedType) {
/** expected type */
return value;
}
if (currentType == null) {
currentType = OPAQUE;
}
if (currentType == expectedType || currentType == NONE) {
/** expected type */
return value;
}
switch (expectedType) {
case INTEGER:
switch (currentType) {

View File

@ -23,9 +23,13 @@ import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class JsonUtils {
private static final Pattern BASE64_PATTERN =
Pattern.compile("^[A-Za-z0-9+/]+={0,2}$");
public static JsonObject getJsonObject(List<KeyValueProto> tsKv) {
JsonObject json = new JsonObject();
for (KeyValueProto kv : tsKv) {
@ -56,7 +60,14 @@ public class JsonUtils {
} else if (value instanceof Long) {
return new JsonPrimitive((Long) value);
} else if (value instanceof String) {
return JsonParser.parseString((String) value);
try {
return JsonParser.parseString((String) value);
} catch (Exception e) {
if (isBase64(value.toString())) {
value = "\"" + value + "\"";
}
return JsonParser.parseString((String) value);
}
} else if (value instanceof Boolean) {
return new JsonPrimitive((Boolean) value);
} else if (value instanceof Double) {
@ -77,4 +88,7 @@ public class JsonUtils {
return jsonObject;
}
public static boolean isBase64(String value) {
return value.length() % 4 == 0 && BASE64_PATTERN.matcher(value).matches();
}
}

View File

@ -52,39 +52,47 @@
<ng-template matTabContent [formGroup]="lwm2mDeviceProfileFormGroup">
<section formGroupName="clientLwM2mSettings">
<fieldset class="fields-group">
<legend class="group-title" translate>device-profile.lwm2m.fw-update</legend>
<mat-form-field class="mat-block flex-1">
<mat-label>{{ 'device-profile.lwm2m.fw-update-strategy' | translate }}</mat-label>
<mat-select formControlName="fwUpdateStrategy">
<mat-option [value]=1>{{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }}</mat-option>
<mat-option [value]=2>{{ 'device-profile.lwm2m.fw-update-strategy-package-uri' | translate }}</mat-option>
<mat-option [value]=3>{{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block flex-1" *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateStrategy').value === 2">
<mat-label>{{ 'device-profile.lwm2m.fw-update-resource' | translate }}</mat-label>
<input matInput formControlName="fwUpdateResource" required>
<mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateResource').hasError('required')">
{{ 'device-profile.lwm2m.fw-update-resource-required' | translate }}
</mat-error>
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>device-profile.lwm2m.sw-update</legend>
<mat-form-field class="mat-block flex-1">
<mat-label>{{ 'device-profile.lwm2m.sw-update-strategy' | translate }}</mat-label>
<mat-select formControlName="swUpdateStrategy">
<mat-option [value]=1>{{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }}</mat-option>
<mat-option [value]=2>{{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block flex-1" *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateStrategy').value === 2">
<mat-label>{{ 'device-profile.lwm2m.sw-update-resource' | translate }}</mat-label>
<input matInput formControlName="swUpdateResource" required>
<mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateResource').hasError('required')">
{{ 'device-profile.lwm2m.sw-update-resource-required' | translate }}
</mat-error>
</mat-form-field>
<legend class="group-title" translate>device-profile.lwm2m.ota-update</legend>
<mat-checkbox formControlName="useObject19ForOta">
<span tb-hint-tooltip-icon="{{ 'device-profile.lwm2m.use-object-19-for-ota-update-hint' | translate }}">
{{ 'device-profile.lwm2m.use-object-19-for-ota-update' | translate }}
</span>
</mat-checkbox>
<fieldset class="fields-group">
<legend class="group-title" translate>device-profile.lwm2m.fw-update</legend>
<mat-form-field class="mat-block flex-1">
<mat-label>{{ 'device-profile.lwm2m.fw-update-strategy' | translate }}</mat-label>
<mat-select formControlName="fwUpdateStrategy">
<mat-option [value]=1>{{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }}</mat-option>
<mat-option [value]=2>{{ 'device-profile.lwm2m.fw-update-strategy-package-uri' | translate }}</mat-option>
<mat-option [value]=3>{{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block flex-1" *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateStrategy').value === 2">
<mat-label>{{ 'device-profile.lwm2m.fw-update-resource' | translate }}</mat-label>
<input matInput formControlName="fwUpdateResource" required>
<mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateResource').hasError('required')">
{{ 'device-profile.lwm2m.fw-update-resource-required' | translate }}
</mat-error>
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>device-profile.lwm2m.sw-update</legend>
<mat-form-field class="mat-block flex-1">
<mat-label>{{ 'device-profile.lwm2m.sw-update-strategy' | translate }}</mat-label>
<mat-select formControlName="swUpdateStrategy">
<mat-option [value]=1>{{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }}</mat-option>
<mat-option [value]=2>{{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block flex-1" *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateStrategy').value === 2">
<mat-label>{{ 'device-profile.lwm2m.sw-update-resource' | translate }}</mat-label>
<input matInput formControlName="swUpdateResource" required>
<mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateResource').hasError('required')">
{{ 'device-profile.lwm2m.sw-update-resource-required' | translate }}
</mat-error>
</mat-form-field>
</fieldset>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>device-profile.power-saving-mode</legend>

View File

@ -104,6 +104,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
bootstrap: [[]],
clientLwM2mSettings: this.fb.group({
clientOnlyObserveAfterConnect: [1, []],
useObject19ForOta: [false],
fwUpdateStrategy: [1, []],
swUpdateStrategy: [1, []],
fwUpdateResource: [{value: '', disabled: true}, []],
@ -262,6 +263,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
bootstrapServerUpdateEnable: this.configurationValue.bootstrapServerUpdateEnable || false,
clientLwM2mSettings: {
clientOnlyObserveAfterConnect: this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect,
useObject19ForOta: this.configurationValue.clientLwM2mSettings.useObject19ForOta ?? false,
fwUpdateStrategy: this.configurationValue.clientLwM2mSettings.fwUpdateStrategy || 1,
swUpdateStrategy: this.configurationValue.clientLwM2mSettings.swUpdateStrategy || 1,
fwUpdateResource: this.configurationValue.clientLwM2mSettings.fwUpdateResource || '',

View File

@ -124,13 +124,15 @@ export const PowerModeTranslationMap = new Map<PowerMode, string>(
export enum ObjectIDVer {
V1_0 = '1.0',
V1_1 = '1.1'
V1_1 = '1.1',
V1_2 = '1.2',
}
export const ObjectIDVerTranslationMap = new Map<ObjectIDVer, string>(
[
[ObjectIDVer.V1_0, 'device-profile.lwm2m.default-object-id-ver.v1-0'],
[ObjectIDVer.V1_1, 'device-profile.lwm2m.default-object-id-ver.v1-1']
[ObjectIDVer.V1_1, 'device-profile.lwm2m.default-object-id-ver.v1-1'],
[ObjectIDVer.V1_2, 'device-profile.lwm2m.default-object-id-ver.v1-2'],
]
);
@ -167,6 +169,7 @@ export interface Lwm2mProfileConfigModels {
export interface ClientLwM2mSettings {
clientOnlyObserveAfterConnect: number;
useObject19ForOta?: boolean;
fwUpdateStrategy: number;
swUpdateStrategy: number;
fwUpdateResource?: string;

View File

@ -2174,6 +2174,9 @@
"add-lwm2m-server-config": "Add LwM2M server",
"no-config-servers": "No servers configured",
"others-tab": "Other settings",
"ota-update": "OTA update",
"use-object-19-for-ota-update": "Use Object 19 for OTA file metadata (checksum, size, version, name)",
"use-object-19-for-ota-update-hint": "Use Resource ObjectId = 19 for OTA updates: FirmWare → InstanceId = 65534, SoftWare → InstanceId = 65535. The data format is JSON wrapped in Base64. This JSON contains OTA file metadata (file info): \"Checksum\" (SHA256). Additional fields: \"Title\" (OTA name), \"Version\" (OTA version), \"File Name\" (file name for storing OTA on the client), \"File Size\" (OTA size in bytes).",
"client-strategy": "Client strategy when connecting",
"client-strategy-label": "Strategy",
"client-strategy-only-observe": "Only Observe Request to the client after the initial connection",
@ -2204,7 +2207,8 @@
"default-object-id": "Default Object Version (Attribute)",
"default-object-id-ver": {
"v1-0": "1.0",
"v1-1": "1.1"
"v1-1": "1.1",
"v1-2": "1.2"
}
},
"snmp": {