diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index b895614bdc..2924f8ddeb 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -257,7 +257,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { 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(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 9bfb067bbd..827112cf2a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -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 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 getBootstrapServerCredentialsNoSec(LwM2MProfileBootstrapConfigType bootstrapConfigType) { List bootstrap = new ArrayList<>(); switch (bootstrapConfigType) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index 4d56e891cc..eb87a9bc3b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -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 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()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java index 587eba6aa3..3065255687 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java @@ -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 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); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java index 8bb367f66e..b2617dd7ba 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java @@ -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 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); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java index 9d64e1a64d..01030c24bd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java @@ -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()); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java index 8eabff8ed8..b056309634 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java @@ -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; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 12bc176fe3..ed685dfc6c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -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 value = convertMultiResourceValuesFromRpcBody(valueForMultiResource, resourceModelWrite.type, request.getObjectId()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 2f2407e451..ea8c2acb14 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -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 fwStates = new ConcurrentHashMap<>(); private final Map 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 getAttributeValue(List 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 toOtaPackageUpdateStatus(FirmwareUpdateResult fwUpdateResult) { switch (fwUpdateResult) { case INITIAL: diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/LwM2MClientOtaInfo.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/LwM2MClientOtaInfo.java index b5ec94d48b..ee1ca92066 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/LwM2MClientOtaInfo.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/LwM2MClientOtaInfo.java @@ -77,7 +77,7 @@ public abstract class LwM2MClientOtaInfo { } 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); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/firmware/LwM2MClientFwOtaInfo.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/firmware/LwM2MClientFwOtaInfo.java index 6f6aae9c45..0ef082e740 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/firmware/LwM2MClientFwOtaInfo.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/firmware/LwM2MClientFwOtaInfo.java @@ -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 UPDATE_SUCCESSFULLY.getCode()) { + failedPackageId = getPackageId(targetName, targetVersion); + } + switch (result) { case INITIAL: break; @@ -50,7 +58,6 @@ public class LwM2MClientFwOtaInfo extends LwM2MClientOtaInfo= 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; } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 0f428ad9af..9976783179 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -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); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index 480f4ed778..91d55305da 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -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) { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java index 28a904edb9..a79d897daf 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java @@ -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 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(); + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 0ad27b23ef..bb37b5784a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -52,39 +52,47 @@
- device-profile.lwm2m.fw-update - - {{ 'device-profile.lwm2m.fw-update-strategy' | translate }} - - {{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }} - {{ 'device-profile.lwm2m.fw-update-strategy-package-uri' | translate }} - {{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }} - - - - {{ 'device-profile.lwm2m.fw-update-resource' | translate }} - - - {{ 'device-profile.lwm2m.fw-update-resource-required' | translate }} - - -
-
- device-profile.lwm2m.sw-update - - {{ 'device-profile.lwm2m.sw-update-strategy' | translate }} - - {{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }} - {{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }} - - - - {{ 'device-profile.lwm2m.sw-update-resource' | translate }} - - - {{ 'device-profile.lwm2m.sw-update-resource-required' | translate }} - - + device-profile.lwm2m.ota-update + + + {{ 'device-profile.lwm2m.use-object-19-for-ota-update' | translate }} + + +
+ device-profile.lwm2m.fw-update + + {{ 'device-profile.lwm2m.fw-update-strategy' | translate }} + + {{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }} + {{ 'device-profile.lwm2m.fw-update-strategy-package-uri' | translate }} + {{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }} + + + + {{ 'device-profile.lwm2m.fw-update-resource' | translate }} + + + {{ 'device-profile.lwm2m.fw-update-resource-required' | translate }} + + +
+
+ device-profile.lwm2m.sw-update + + {{ 'device-profile.lwm2m.sw-update-strategy' | translate }} + + {{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }} + {{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }} + + + + {{ 'device-profile.lwm2m.sw-update-resource' | translate }} + + + {{ 'device-profile.lwm2m.sw-update-resource-required' | translate }} + + +
device-profile.power-saving-mode diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index a9fbb7b249..46c439cf33 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -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 || '', diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index e5706cf39b..33b358e581 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -124,13 +124,15 @@ export const PowerModeTranslationMap = new Map( 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.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; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 72afcfc0b9..b160a44b0e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -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": {