From 542f647f26643329746a543c543aaf1649b58fd8 Mon Sep 17 00:00:00 2001 From: yunhanw-google Date: Thu, 24 Aug 2023 20:24:14 -0700 Subject: [PATCH] update json/tlv for report (#28639) --- .../pairing/PairOnNetworkLongImReadCommand.kt | 113 +++++++++++++++--- src/controller/java/AndroidCallbacks.cpp | 53 +++++--- .../devicecontroller/model/ClusterState.java | 39 +++++- 3 files changed, 174 insertions(+), 31 deletions(-) diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt index f432027c6cbdd0..bdf43dfe02a1d5 100644 --- a/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt +++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/pairing/PairOnNetworkLongImReadCommand.kt @@ -3,11 +3,13 @@ package com.matter.controller.commands.pairing import chip.devicecontroller.ChipDeviceController import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback import chip.devicecontroller.ReportCallback +import chip.devicecontroller.model.AttributeState import chip.devicecontroller.model.ChipAttributePath import chip.devicecontroller.model.ChipEventPath +import chip.devicecontroller.model.ChipPathId +import chip.devicecontroller.model.EventState import chip.devicecontroller.model.NodeState import com.matter.controller.commands.common.CredentialsIssuer -import java.util.Collections import java.util.logging.Level import java.util.logging.Logger @@ -35,9 +37,83 @@ class PairOnNetworkLongImReadCommand( setFailure("read failure") } + // kotlin-detect complains that bytearray as a magic number, but we cannot define bytearray + // as a well named constant and const can only support with primitive and string. + @Suppress("MagicNumber") + fun checkLocalConfigDisableAttributeTlv(attribute: AttributeState): Boolean = + attribute.getTlv().contentEquals(byteArrayOf(0x8)) + + fun checkLocalConfigDisableAttributeJson(attribute: AttributeState): Boolean = + attribute.getJson().toString() == """{"16:BOOL":false}""" + + // kotlin-detect complains that bytearray as a magic number, but we cannot define bytearray + // as a well named constant and const can only support with primitive and string. + @Suppress("MagicNumber") + fun checkStartUpEventTlv(event: EventState): Boolean = + event.getTlv().contentEquals(byteArrayOf(0x15, 0x24, 0x0, 0x1, 0x18)) + + fun checkStartUpEventJson(event: EventState): Boolean = + event.getJson().toString() == """{"0:STRUCT":{"0:UINT":1}}""" + + fun checkAllAttributesJsonForBasicCluster(cluster: String): Boolean { + val expected = + """{"16:BOOL":false,""" + + """"65531:ARRAY-UINT":[""" + + """0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,65528,65529,65531,65532,65533]}""" + return cluster.equals(expected) + } + + private fun validateResponse(nodeState: NodeState) { + val endpointZero = + requireNotNull(nodeState.getEndpointState(0)) { "Endpoint zero not found." } + + val basicCluster = + requireNotNull(endpointZero.getClusterState(CLUSTER_ID_BASIC)) { + "Basic cluster not found." + } + + val localConfigDisabledAttribute = + requireNotNull(basicCluster.getAttributeState(ATTR_ID_LOCAL_CONFIG_DISABLED)) { + "No local config disabled attribute found." + } + + val startUpEvents = + requireNotNull(basicCluster.getEventState(EVENT_ID_START_UP)) { "No start up event found." } + + val clusterAttributes = + requireNotNull(basicCluster.getAttributesJson()) { "No basicCluster attribute found." } + + require(checkLocalConfigDisableAttributeTlv(localConfigDisabledAttribute)) { + "Invalid local config disabled attribute TLV ${localConfigDisabledAttribute.getTlv().contentToString()}" + } + + require(checkLocalConfigDisableAttributeJson(localConfigDisabledAttribute)) { + "Invalid local config disabled attribute Json ${localConfigDisabledAttribute.getJson().toString()}" + } + + require(startUpEvents.isNotEmpty()) { "Unexpected: startUpEvents is empty" } + + require(checkStartUpEventTlv(startUpEvents[0])) { + "Invalid start up event TLV ${startUpEvents[0].getTlv().contentToString()}" + } + + require(checkStartUpEventJson(startUpEvents[0])) { + "Invalid start up event Json ${startUpEvents[0].getJson().toString()}" + } + + require(checkAllAttributesJsonForBasicCluster(clusterAttributes)) { + "Invalid basic cluster attributes Json ${clusterAttributes}" + } + } + override fun onReport(nodeState: NodeState) { - logger.log(Level.INFO, "Read receve onReport") - setSuccess() + logger.log(Level.INFO, nodeState.toString()) + try { + validateResponse(nodeState) + setSuccess() + } catch (ex: IllegalArgumentException) { + setFailure(ex.message) + } } } @@ -56,9 +132,23 @@ class PairOnNetworkLongImReadCommand( val attributePathList = listOf( ChipAttributePath.newInstance( - /* endpointId= */ 0, - CLUSTER_ID_BASIC, - ATTR_ID_LOCAL_CONFIG_DISABLED + ChipPathId.forId(/* endpointId= */ 0), + ChipPathId.forId(CLUSTER_ID_BASIC), + ChipPathId.forId(ATTR_ID_LOCAL_CONFIG_DISABLED), + ), + ChipAttributePath.newInstance( + ChipPathId.forId(/* endpointId= */ 0), + ChipPathId.forId(CLUSTER_ID_BASIC), + ChipPathId.forId(GLOBAL_ATTRIBUTE_LIST), + ) + ) + + val eventPathList = + listOf( + ChipEventPath.newInstance( + ChipPathId.forWildcard(), + ChipPathId.forWildcard(), + ChipPathId.forWildcard() ) ) @@ -77,14 +167,7 @@ class PairOnNetworkLongImReadCommand( .getConnectedDevicePointer(getNodeId(), InternalGetConnectedDeviceCallback()) clear() currentCommissioner() - .readPath( - InternalReportCallback(), - devicePointer, - attributePathList, - Collections.emptyList(), - false, - 0 - ) + .readPath(InternalReportCallback(), devicePointer, attributePathList, eventPathList, false, 0) waitCompleteMs(getTimeoutMillis()) } @@ -94,5 +177,7 @@ class PairOnNetworkLongImReadCommand( private const val MATTER_PORT = 5540 private const val CLUSTER_ID_BASIC = 0x0028L private const val ATTR_ID_LOCAL_CONFIG_DISABLED = 16L + private const val EVENT_ID_START_UP = 0L + private const val GLOBAL_ATTRIBUTE_LIST = 65531L } } diff --git a/src/controller/java/AndroidCallbacks.cpp b/src/controller/java/AndroidCallbacks.cpp index daa7f9ef8abcb0..1598567e212880 100644 --- a/src/controller/java/AndroidCallbacks.cpp +++ b/src/controller/java/AndroidCallbacks.cpp @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -40,6 +39,8 @@ namespace Controller { static const int MILLIS_SINCE_BOOT = 0; static const int MILLIS_SINCE_EPOCH = 1; +// Add the bytes for attribute tag(1:control + 8:tag + 8:length) and structure(1:struct + 1:close container) +static const int EXTRA_SPACE_FOR_ATTRIBUTE_TAG = 19; GetConnectedDeviceCallback::GetConnectedDeviceCallback(jobject wrapperCallback, jobject javaCallback) : mOnSuccess(OnDeviceConnectedFn, this), mOnFailure(OnDeviceConnectionFailureFn, this) @@ -224,6 +225,32 @@ void ReportCallback::OnReportEnd() VerifyOrReturn(!env->ExceptionCheck(), env->ExceptionDescribe()); } +// Convert TLV blob to Json with structure, the element's tag is replaced with the actual attributeId. +CHIP_ERROR ConvertReportTlvToJson(const uint32_t id, TLV::TLVReader & data, std::string & json) +{ + TLV::TLVWriter writer; + TLV::TLVReader readerForJavaTLV; + uint32_t size = 0; + size_t bufferLen = readerForJavaTLV.GetTotalLength() + EXTRA_SPACE_FOR_ATTRIBUTE_TAG; + readerForJavaTLV.Init(data); + std::unique_ptr buffer = std::unique_ptr(new uint8_t[bufferLen]); + writer.Init(buffer.get(), bufferLen); + TLV::TLVType outer; + + ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outer)); + TLV::Tag tag; + ReturnErrorOnFailure(ConvertTlvTag(id, tag)); + ReturnErrorOnFailure(writer.CopyElement(tag, readerForJavaTLV)); + ReturnErrorOnFailure(writer.EndContainer(outer)); + size = writer.GetLengthWritten(); + + TLV::TLVReader readerForJson; + readerForJson.Init(buffer.get(), size); + ReturnErrorOnFailure(readerForJson.Next()); + // Convert TLV to JSON + return TlvToJson(readerForJson, json); +} + void ReportCallback::OnAttributeData(const app::ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const app::StatusIB & aStatus) { @@ -252,9 +279,7 @@ void ReportCallback::OnAttributeData(const app::ConcreteDataAttributePath & aPat } TLV::TLVReader readerForJavaTLV; - TLV::TLVReader readerForJson; readerForJavaTLV.Init(*apData); - readerForJson.Init(*apData); jobject value = nullptr; #if USE_JAVA_TLV_ENCODE_DECODE @@ -276,6 +301,7 @@ void ReportCallback::OnAttributeData(const app::ConcreteDataAttributePath & aPat size_t bufferLen = readerForJavaTLV.GetRemainingLength() + readerForJavaTLV.GetLengthRead(); std::unique_ptr buffer = std::unique_ptr(new uint8_t[bufferLen]); uint32_t size = 0; + // The TLVReader's read head is not pointing to the first element in the container, instead of the container itself, use // a TLVWriter to get a TLV with a normalized TLV buffer (Wrapped with an anonymous tag, no extra "end of container" tag // at the end.) @@ -287,11 +313,10 @@ void ReportCallback::OnAttributeData(const app::ConcreteDataAttributePath & aPat chip::ByteArray jniByteArray(env, reinterpret_cast(buffer.get()), size); // Convert TLV to JSON - Json::Value json; - err = TlvToJson(readerForJson, json); + std::string json; + err = ConvertReportTlvToJson(static_cast(aPath.mAttributeId), *apData, json); VerifyOrReturn(err == CHIP_NO_ERROR, ReportError(attributePathObj, nullptr, err)); - - UtfString jsonString(env, JsonToString(json).c_str()); + UtfString jsonString(env, json.c_str()); // Create AttributeState object jclass attributeStateCls; @@ -366,9 +391,7 @@ void ReportCallback::OnEventData(const app::EventHeader & aEventHeader, TLV::TLV } TLV::TLVReader readerForJavaTLV; - TLV::TLVReader readerForJson; readerForJavaTLV.Init(*apData); - readerForJson.Init(*apData); jlong eventNumber = static_cast(aEventHeader.mEventNumber); jint priorityLevel = static_cast(aEventHeader.mPriorityLevel); @@ -420,11 +443,10 @@ void ReportCallback::OnEventData(const app::EventHeader & aEventHeader, TLV::TLV chip::ByteArray jniByteArray(env, reinterpret_cast(buffer.get()), size); // Convert TLV to JSON - Json::Value json; - err = TlvToJson(readerForJson, json); - VerifyOrReturn(err == CHIP_NO_ERROR, ReportError(nullptr, eventPathObj, err)); - - UtfString jsonString(env, JsonToString(json).c_str()); + std::string json; + err = ConvertReportTlvToJson(static_cast(aEventHeader.mPath.mEventId), *apData, json); + VerifyOrReturn(err == CHIP_NO_ERROR, ReportError(eventPathObj, nullptr, err)); + UtfString jsonString(env, json.c_str()); // Create EventState object jclass eventStateCls; @@ -492,7 +514,8 @@ CHIP_ERROR InvokeCallback::CreateInvokeElement(const app::ConcreteCommandPath & readerForJavaTLV.Init(*apData); // Create TLV byte array to pass to Java layer - size_t bufferLen = readerForJavaTLV.GetRemainingLength() + readerForJavaTLV.GetLengthRead(); + size_t bufferLen = readerForJavaTLV.GetRemainingLength() + readerForJavaTLV.GetLengthRead(); + ; std::unique_ptr buffer = std::unique_ptr(new uint8_t[bufferLen]); uint32_t size = 0; diff --git a/src/controller/java/src/chip/devicecontroller/model/ClusterState.java b/src/controller/java/src/chip/devicecontroller/model/ClusterState.java index 8507682968edc5..4c575f1a41df66 100644 --- a/src/controller/java/src/chip/devicecontroller/model/ClusterState.java +++ b/src/controller/java/src/chip/devicecontroller/model/ClusterState.java @@ -17,13 +17,19 @@ */ package chip.devicecontroller.model; +import android.util.Log; import java.util.ArrayList; +import java.util.Iterator; import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import javax.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; /** Class for tracking CHIP cluster state in a hierarchical manner. */ public final class ClusterState { + private static final String TAG = "ClusterState"; private Map attributes; private Map> events; private Optional dataVersion; @@ -51,6 +57,35 @@ public Optional getDataVersion() { return dataVersion; } + /** + * Convenience utility for getting all attributes in Json string format. + * + * @return all attributes in Json string format., or empty string if not found. + */ + public String getAttributesJson() { + JSONObject combinedObject = new JSONObject(); + Stream attributeJsons = + attributes.values().stream().map(it -> it.getJson()).filter(it -> it != null); + + attributeJsons.forEach( + attributes -> { + for (Iterator iterator = attributes.keys(); iterator.hasNext(); ) { + String key = iterator.next(); + if (combinedObject.has(key)) { + Log.e(TAG, "Conflicting attribute tag Id is found: " + key); + continue; + } + try { + Object value = attributes.get(key); + combinedObject.put(key, value); + } catch (JSONException ex) { + Log.e(TAG, "receive attribute json exception: " + ex); + } + } + }); + return combinedObject.toString(); + } + /** * Convenience utility for getting an {@code AttributeState}. * @@ -80,7 +115,7 @@ public String toString() { builder.append(attributeId); builder.append(": "); builder.append( - attributeState.getValue() == null ? "null" : attributeState.getValue().toString()); + attributeState.getJson() == null ? "null" : attributeState.getJson().toString()); builder.append("\n"); }); events.forEach( @@ -91,7 +126,7 @@ public String toString() { builder.append(eventId); builder.append(": "); builder.append( - eventState.getValue() == null ? "null" : eventState.getValue().toString()); + eventState.getJson() == null ? "null" : eventState.getJson().toString()); builder.append("\n"); }); });