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:
commit
300cdbe0f7
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 || '',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user