blob: 8ccb2591e45e8f7a4033826969bd76e6aa394ffe [file] [log] [blame]
package com.google.chip.chiptool.clusterclient
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import chip.devicecontroller.ChipDeviceController
import chip.devicecontroller.ReportCallback
import chip.devicecontroller.model.ChipAttributePath
import chip.devicecontroller.model.ChipEventPath
import chip.devicecontroller.model.NodeState
import com.google.chip.chiptool.ChipClient
import com.google.chip.chiptool.R
import com.google.chip.chiptool.databinding.SensorClientFragmentBinding
import com.google.chip.chiptool.util.DeviceIdUtil
import com.google.chip.chiptool.util.TlvParseUtil
import com.jjoe64.graphview.LabelFormatter
import com.jjoe64.graphview.Viewport
import com.jjoe64.graphview.series.DataPoint
import com.jjoe64.graphview.series.LineGraphSeries
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import chip.devicecontroller.ClusterIDMapping.*
class SensorClientFragment : Fragment() {
private val deviceController: ChipDeviceController
get() = ChipClient.getDeviceController(requireContext())
private lateinit var scope: CoroutineScope
// History of sensor values
private val sensorData = LineGraphSeries<DataPoint>()
// Device whose attribute is subscribed
private var subscribedDevicePtr = 0L
private var _binding: SensorClientFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = SensorClientFragmentBinding.inflate(inflater, container, false)
scope = viewLifecycleOwner.lifecycleScope
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ChipClient.getDeviceController(requireContext()).setCompletionListener(null)
binding.deviceIdEd.setOnEditorActionListener { textView, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
resetSensorGraph() // reset the graph on device change
}
actionId == EditorInfo.IME_ACTION_DONE
}
binding.endpointIdEd.setOnEditorActionListener { textView, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE)
resetSensorGraph() // reset the graph on endpoint change
actionId == EditorInfo.IME_ACTION_DONE
}
binding.clusterNameSpinner.adapter = makeClusterNamesAdapter()
binding.clusterNameSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
resetSensorGraph() // reset the graph on cluster change
}
}
binding.readSensorBtn.setOnClickListener { scope.launch { readSensorCluster() } }
binding.watchSensorBtn.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
scope.launch { subscribeSensorCluster() }
} else {
unsubscribeSensorCluster()
}
}
val currentTime = Calendar.getInstance().time.time
binding.sensorGraph.addSeries(sensorData)
binding.sensorGraph.viewport.isXAxisBoundsManual = true
binding.sensorGraph.viewport.setMinX(currentTime.toDouble())
binding.sensorGraph.viewport.setMaxX(currentTime.toDouble() + MIN_REFRESH_PERIOD_S * 1000 * MAX_DATA_POINTS)
binding.sensorGraph.gridLabelRenderer.padding = 30
binding.sensorGraph.gridLabelRenderer.numHorizontalLabels = 4
binding.sensorGraph.gridLabelRenderer.setHorizontalLabelsAngle(150)
binding.sensorGraph.gridLabelRenderer.labelFormatter = object : LabelFormatter {
override fun setViewport(viewport: Viewport?) = Unit
override fun formatLabel(value: Double, isValueX: Boolean): String {
if (isValueX)
return SimpleDateFormat("H:mm:ss").format(Date(value.toLong())).toString()
if (value >= 100.0)
return "%.1f".format(value)
return "%.2f".format(value)
}
}
}
override fun onStart() {
super.onStart()
binding.deviceIdEd.setText(DeviceIdUtil.getLastDeviceId(requireContext()).toString())
}
override fun onStop() {
resetSensorGraph() // reset the graph on fragment exit
super.onStop()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun resetSensorGraph() {
binding.watchSensorBtn.isChecked = false
binding.sensorGraph.visibility = View.INVISIBLE
sensorData.resetData(emptyArray())
}
private fun makeClusterNamesAdapter(): ArrayAdapter<String> {
return ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item,
CLUSTERS.keys.toList()
).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
}
private suspend fun readSensorCluster() {
try {
val deviceId = binding.deviceIdEd.text.toString().toULong().toLong()
val endpointId = binding.endpointIdEd.text.toString().toInt()
val clusterName = binding.clusterNameSpinner.selectedItem.toString()
val clusterId = CLUSTERS[clusterName]!!["clusterId"] as Long
val attributeId = CLUSTERS[clusterName]!!["attributeId"] as Long
val device = ChipClient.getConnectedDevicePointer(requireContext(), deviceId)
val callback = makeReadCallback(clusterName, false)
deviceController.readAttributePath(callback, device, listOf(ChipAttributePath.newInstance(endpointId.toLong(), clusterId, attributeId)), 0)
} catch (ex: Exception) {
Log.d(TAG, "Failed to read the sensor : ", ex)
showMessage(R.string.sensor_client_read_error_text, ex.toString())
}
}
private suspend fun subscribeSensorCluster() {
try {
val deviceId = binding.deviceIdEd.text.toString().toULong().toLong()
val endpointId = binding.endpointIdEd.text.toString().toInt()
val clusterName = binding.clusterNameSpinner.selectedItem.toString()
val clusterId = CLUSTERS[clusterName]!!["clusterId"] as Long
val attributeId = CLUSTERS[clusterName]!!["attributeId"] as Long
val device = ChipClient.getConnectedDevicePointer(requireContext(), deviceId)
val callback = makeReadCallback(clusterName, true)
deviceController.subscribeToAttributePath({ Log.d(TAG, "onSubscriptionEstablished") }, callback, device, listOf(ChipAttributePath.newInstance(endpointId.toLong(), clusterId, attributeId)), MIN_REFRESH_PERIOD_S, MAX_REFRESH_PERIOD_S, 0)
subscribedDevicePtr = device
} catch (ex: Exception) {
Log.d(TAG, "Failed to subscribe", ex)
showMessage(R.string.sensor_client_subscribe_error_text, ex.toString())
}
}
private fun unsubscribeSensorCluster() {
if (subscribedDevicePtr == 0L)
return
try {
ChipClient.getDeviceController(requireContext()).shutdownSubscriptions()
subscribedDevicePtr = 0
} catch (ex: Exception) {
showMessage(R.string.sensor_client_unsubscribe_error_text, ex.toString())
}
}
private fun makeReadCallback(clusterName: String, addToGraph: Boolean): ReportCallback {
return object : ReportCallback {
val clusterConfig = CLUSTERS[clusterName]!!
val endpointId = binding.endpointIdEd.text.toString().toInt()
val clusterId = clusterConfig["clusterId"] as Long
val attributeId = clusterConfig["attributeId"] as Long
override fun onReport(nodeState: NodeState?) {
val tlv = nodeState?.getEndpointState(endpointId)?.getClusterState(clusterId)?.getAttributeState(attributeId)?.tlv ?: return
// TODO : Need to be implement poj-to-tlv
val value =
try {
TlvParseUtil.decodeInt(tlv)
} catch (ex: Exception) {
showMessage(R.string.sensor_client_read_error_text, "value is null")
return
}
val unitValue = clusterConfig["unitValue"] as Double
val unitSymbol = clusterConfig["unitSymbol"] as String
consumeSensorValue(value * unitValue, unitSymbol, addToGraph)
}
override fun onError(attributePath: ChipAttributePath?, eventPath: ChipEventPath?, ex: Exception) {
showMessage(R.string.sensor_client_read_error_text, ex.toString())
}
}
}
private fun consumeSensorValue(value: Double, unitSymbol: String, addToGraph: Boolean) {
requireActivity().runOnUiThread {
binding.lastValueTv.text = requireContext().getString(
R.string.sensor_client_last_value_text, value, unitSymbol
)
if (addToGraph) {
val isFirstSample = sensorData.isEmpty
val dataPoint = DataPoint(Calendar.getInstance().time, value)
sensorData.appendData(dataPoint, true, MAX_DATA_POINTS)
if (isFirstSample) {
// Make the graph visible on the first sample. Also, workaround a bug in graphview
// related to calculating the viewport when there is only one data point by
// duplicating the first sample.
sensorData.appendData(dataPoint, true, MAX_DATA_POINTS)
binding.sensorGraph.visibility = View.VISIBLE
}
}
}
}
private fun showMessage(msgResId: Int, stringArgs: String? = null) {
requireActivity().runOnUiThread {
val context = requireContext()
val message = context.getString(msgResId, stringArgs)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
Log.i(TAG, message)
}
}
companion object {
private const val TAG = "SensorClientFragment"
private const val MIN_REFRESH_PERIOD_S = 2
private const val MAX_REFRESH_PERIOD_S = 10
private const val MAX_DATA_POINTS = 60
private val CLUSTERS = mapOf(
"Temperature" to mapOf(
"clusterId" to TemperatureMeasurement.ID,
"attributeId" to TemperatureMeasurement.Attribute.MeasuredValue.id,
"unitValue" to 0.01,
"unitSymbol" to "\u00B0C"
),
"Pressure" to mapOf(
"clusterId" to PressureMeasurement.ID,
"attributeId" to PressureMeasurement.Attribute.MeasuredValue.id,
"unitValue" to 1.0,
"unitSymbol" to "hPa"
),
"Relative Humidity" to mapOf(
"clusterId" to RelativeHumidityMeasurement.ID,
"attributeId" to RelativeHumidityMeasurement.Attribute.MeasuredValue.id,
"unitValue" to 0.01,
"unitSymbol" to "%"
)
)
fun newInstance(): SensorClientFragment = SensorClientFragment()
}
}