blob: 2d4abab5b668e9f64bb223b42eda800cb70eec2f [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.ChipClusters
import com.google.chip.chiptool.ChipClient
import com.google.chip.chiptool.R
import com.google.chip.chiptool.databinding.AttestationTestFragmentBinding
import com.google.chip.chiptool.databinding.SensorClientFragmentBinding
import com.google.chip.chiptool.util.DeviceIdUtil
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
private typealias ReadCallback = ChipClusters.IntegerAttributeCallback
private typealias TemperatureReadCallback =
ChipClusters.TemperatureMeasurementCluster.MeasuredValueAttributeCallback
private typealias PressureReadCallback =
ChipClusters.PressureMeasurementCluster.MeasuredValueAttributeCallback
private typealias RelativeHumidityReadCallback =
ChipClusters.RelativeHumidityMeasurementCluster.MeasuredValueAttributeCallback
class SensorClientFragment : Fragment() {
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 clusterRead = CLUSTERS[clusterName]!!["read"] as (Long, Int, ReadCallback) -> Unit
val device = ChipClient.getConnectedDevicePointer(requireContext(), deviceId)
val callback = makeReadCallback(clusterName, false)
clusterRead(device, endpointId, callback)
} catch (ex: Exception) {
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 clusterSubscribe = CLUSTERS[clusterName]!!["subscribe"] as (Long, Int, ReadCallback) -> Unit
val device = ChipClient.getConnectedDevicePointer(requireContext(), deviceId)
val callback = makeReadCallback(clusterName, true)
clusterSubscribe(device, endpointId, callback)
subscribedDevicePtr = device
} catch (ex: Exception) {
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): ReadCallback {
return object : ReadCallback {
val clusterConfig = CLUSTERS[clusterName]!!
override fun onSuccess(value: Int) {
val unitValue = clusterConfig["unitValue"] as Double
val unitSymbol = clusterConfig["unitSymbol"] as String
consumeSensorValue(value * unitValue, unitSymbol, addToGraph)
}
override fun onError(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(
"read" to { device: Long, endpointId: Int, callback: ReadCallback ->
val cluster = ChipClusters.TemperatureMeasurementCluster(device, endpointId)
cluster.readMeasuredValueAttribute(makeTemperatureReadCallback(callback))
},
"subscribe" to { device: Long, endpointId: Int, callback: ReadCallback ->
val cluster = ChipClusters.TemperatureMeasurementCluster(device, endpointId)
cluster.subscribeMeasuredValueAttribute(makeTemperatureReadCallback(callback),
MIN_REFRESH_PERIOD_S,
MAX_REFRESH_PERIOD_S)
},
"unitValue" to 0.01,
"unitSymbol" to "\u00B0C"
),
"Pressure" to mapOf(
"read" to { device: Long, endpointId: Int, callback: ReadCallback ->
val cluster = ChipClusters.PressureMeasurementCluster(device, endpointId)
cluster.readMeasuredValueAttribute(makePressureReadCallback(callback))
},
"subscribe" to { device: Long, endpointId: Int, callback: ReadCallback ->
val cluster = ChipClusters.PressureMeasurementCluster(device, endpointId)
cluster.subscribeMeasuredValueAttribute(makePressureReadCallback(callback),
MIN_REFRESH_PERIOD_S,
MAX_REFRESH_PERIOD_S)
},
"unitValue" to 1.0,
"unitSymbol" to "hPa"
),
"Relative Humidity" to mapOf(
"read" to { device: Long, endpointId: Int, callback: ReadCallback ->
val cluster = ChipClusters.RelativeHumidityMeasurementCluster(device, endpointId)
cluster.readMeasuredValueAttribute(makeHumidityReadCallback(callback))
},
"subscribe" to { device: Long, endpointId: Int, callback: ReadCallback ->
val cluster = ChipClusters.RelativeHumidityMeasurementCluster(device, endpointId)
cluster.subscribeMeasuredValueAttribute(makeHumidityReadCallback(callback),
MIN_REFRESH_PERIOD_S,
MAX_REFRESH_PERIOD_S)
},
"unitValue" to 0.01,
"unitSymbol" to "%"
)
)
private fun makeTemperatureReadCallback(callback: ReadCallback): TemperatureReadCallback {
return object : TemperatureReadCallback {
override fun onSuccess(value: Int?) {
value?.let { callback.onSuccess(it) }
}
override fun onError(error: java.lang.Exception?) {
callback.onError(error)
}
}
}
private fun makePressureReadCallback(callback: ReadCallback): PressureReadCallback {
return object : PressureReadCallback {
override fun onSuccess(value: Int?) {
value?.let { callback.onSuccess(it) }
}
override fun onError(error: java.lang.Exception?) {
callback.onError(error)
}
}
}
private fun makeHumidityReadCallback(callback: ReadCallback): RelativeHumidityReadCallback {
return object : RelativeHumidityReadCallback {
override fun onSuccess(value: Int?) {
value?.let { callback.onSuccess(it) }
}
override fun onError(error: java.lang.Exception?) {
callback.onError(error)
}
}
}
fun newInstance(): SensorClientFragment = SensorClientFragment()
}
}