Skip to content

Commit

Permalink
[ci,citool] get property data when an item is selected. Reorganize la…
Browse files Browse the repository at this point in the history
…yout.
  • Loading branch information
atsushieno committed Dec 26, 2023
1 parent b6e67e3 commit 4b4a913
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ class MidiCIInitiator(val device: MidiCIDeviceInfo,
))
}

fun sendGetPropertyData(destinationMUID: Int, resource: String) {
val conn = connections[destinationMUID]
if (conn != null) {
val header = conn.propertyClient.createRequestHeader(resource)
sendGetPropertyData(Message.GetPropertyData(muid, destinationMUID, requestIdSerial++, header))
}
}

fun sendGetPropertyData(msg: Message.GetPropertyData) {
val conn = connections[msg.destinationMUID]
conn?.addPendingRequest(msg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package dev.atsushieno.ktmidi.ci
* - The list acquisition must be automatically detected.
*/
interface MidiCIPropertyClient {
fun createRequestHeader(resourceIdentifier: String): List<Byte>

fun getPropertyIdForHeader(header: List<Byte>): String

Expand All @@ -21,5 +22,7 @@ interface MidiCIPropertyClient {

fun getReplyStatusFor(header: List<Byte>): Int?

fun getMediaTypeFor(replyHeader: List<Byte>): String?

val propertyCatalogUpdated: MutableList<() -> Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package dev.atsushieno.ktmidi.ci
* Observable list of MIDI-CI Properties
*/
class ObservablePropertyList(private val propertyClient: MidiCIPropertyClient) {
data class Entry(val id: String, val header: List<Byte>, val body: List<Byte>)
data class Entry(val id: String, val replyHeader: List<Byte>, val body: List<Byte>)

private val properties = mutableListOf<Entry>()

Expand All @@ -13,6 +13,7 @@ class ObservablePropertyList(private val propertyClient: MidiCIPropertyClient) {

fun getPropertyIdFor(header: List<Byte>) = propertyClient.getPropertyIdForHeader(header)
fun getReplyStatusFor(header: List<Byte>) = propertyClient.getReplyStatusFor(header)
fun getMediaTypeFor(replyHeader: List<Byte>) = propertyClient.getMediaTypeFor(replyHeader)

fun getPropertyList(): List<PropertyResource>? = propertyClient.getPropertyList()
fun getProperty(header: List<Byte>): List<Byte>? = getProperty(getPropertyIdFor(header))
Expand All @@ -24,7 +25,7 @@ class ObservablePropertyList(private val propertyClient: MidiCIPropertyClient) {
fun set(request: Message.GetPropertyData, reply: Message.GetPropertyDataReply) {
val id = getPropertyIdFor(request.header)
properties.removeAll { it.id == id }
val entry = Entry(id, request.header, reply.body)
val entry = Entry(id, reply.header, reply.body)
properties.add(entry)
propertyChanged.forEach { it(entry) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ object PropertyCommonHeaderKeys {
const val STATUS = "status"
const val MESSAGE = "message"
const val CACHE_TIME = "cacheTime"
// M2-103-UM 5.5 Extra Header Property for Using Property Data which is Not JSON Data
const val MEDIA_TYPE = "mediaType"
}

object CommonRulesKnownMimeTypes {
const val APPLICATION_JSON = "application/json"
}

object PropertyExchangeStatus {
Expand Down Expand Up @@ -109,7 +115,9 @@ data class PropertyCommonReplyHeader(
val status: Int,
val message: String? = null,
val mutualEncoding: String? = PropertyDataEncoding.ASCII,
val cacheTime: String? = null
val cacheTime: String? = null,
// M2-103-UM 5.5 Extra Header Property for Using Property Data which is Not JSON Data
val mediaType: String? = null
)

object PropertyCommonConverter {
Expand Down Expand Up @@ -251,20 +259,31 @@ object CommonRulesPropertyHelper {
return resId ?: resource ?: ""
}

fun getResourceListRequestJson(): Json.JsonValue {
fun getResourceListRequestJson() = createRequestHeader(PropertyResourceNames.RESOURCE_LIST)

fun getResourceListRequestBytes(): List<Byte> {
val json = getResourceListRequestJson()
val requestASCIIBytes = Json.getEscapedString(Json.serialize(json)).toByteArray().toList()
return requestASCIIBytes
}

fun createRequestHeader(resourceIdentifier: String): Json.JsonValue {
val headerContent = Pair(
Json.JsonValue(PropertyCommonHeaderKeys.RESOURCE),
Json.JsonValue(PropertyResourceNames.RESOURCE_LIST))
Json.JsonValue(resourceIdentifier))
return Json.JsonValue(mapOf(headerContent))
}
fun getResourceListRequestBytes(): List<Byte> {
val json = getResourceListRequestJson()

fun createRequestHeaderBytes(resourceIdentifier: String): List<Byte> {
val json = createRequestHeader(resourceIdentifier)
val requestASCIIBytes = Json.getEscapedString(Json.serialize(json)).toByteArray().toList()
return requestASCIIBytes
}
}

class CommonRulesPropertyClient(private val muid: Int, private val sendGetPropertyData: (msg: Message.GetPropertyData) -> Unit) : MidiCIPropertyClient {
override fun createRequestHeader(resourceIdentifier: String): List<Byte> = CommonRulesPropertyHelper.createRequestHeaderBytes(resourceIdentifier)

override fun getPropertyIdForHeader(header: List<Byte>) = CommonRulesPropertyHelper.getPropertyIdentifier(header)

override fun getPropertyList(): List<PropertyResource> = resourceList
Expand All @@ -280,11 +299,27 @@ class CommonRulesPropertyClient(private val muid: Int, private val sendGetProper
propertyCatalogUpdated.forEach { it() }
}

override fun getReplyStatusFor(header: List<Byte>): Int? {
private fun getReplyHeaderField(header: List<Byte>, field: String): Json.JsonValue? {
if (header.isEmpty())
return null
val replyString = Json.getUnescapedString(header.toByteArray().decodeToString())
val replyJson = Json.parse(replyString)
val statusPair = replyJson.token.map.toList().firstOrNull { Json.getUnescapedString(it.first) == PropertyCommonHeaderKeys.STATUS } ?: return null
return if (statusPair.second.token.type == Json.TokenType.Number) statusPair.second.token.number.toInt() else null
val valuePair = replyJson.token.map.toList().firstOrNull { Json.getUnescapedString(it.first) == field } ?: return null
return valuePair.second
}

override fun getReplyStatusFor(header: List<Byte>): Int? {
val json = getReplyHeaderField(header, PropertyCommonHeaderKeys.STATUS)
return if (json == null) null
else if (json.token.type == Json.TokenType.Number) json.token.number.toInt()
else null
}

override fun getMediaTypeFor(replyHeader: List<Byte>): String? {
val json = getReplyHeaderField(replyHeader, PropertyCommonHeaderKeys.MEDIA_TYPE)
return if (json == null) null
else if (json.token.type == Json.TokenType.String) Json.getUnescapedString(json)
else null
}

override val propertyCatalogUpdated = mutableListOf<() -> Unit>()
Expand Down Expand Up @@ -427,6 +462,8 @@ class CommonRulesPropertyService(private val muid: Int, private val deviceInfo:
this[Json.JsonValue(PropertyCommonHeaderKeys.MUTUAL_ENCODING)] = Json.JsonValue(src.mutualEncoding)
if (src.cacheTime != null)
this[Json.JsonValue(PropertyCommonHeaderKeys.CACHE_TIME)] = Json.JsonValue(src.cacheTime)
if (src.mediaType != null)
this[Json.JsonValue(PropertyCommonHeaderKeys.MEDIA_TYPE)] = Json.JsonValue(src.mediaType)
})

fun getPropertyData(headerJson: Json.JsonValue): Pair<Json.JsonValue,Json.JsonValue> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,9 @@ class CIInitiatorModel(private val outputSender: (ciBytes: List<Byte>) -> Unit)
initiator.setProfileOff(msg)
}
}

fun sendGetPropertyDataRequest(destinationMUID: Int, resource: String) {
initiator.sendGetPropertyData(destinationMUID, resource)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@ package dev.atsushieno.ktmidi.citool.view

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import dev.atsushieno.ktmidi.ci.Json
import dev.atsushieno.ktmidi.ci.MidiCIProfile
import dev.atsushieno.ktmidi.ci.MidiCIProfileId
import dev.atsushieno.ktmidi.ci.*
import dev.atsushieno.ktmidi.citool.AppModel

@Composable
Expand All @@ -28,30 +24,24 @@ fun InitiatorScreen() {
}
MidiDeviceSelector()
}
val destinationMUID by remember { ViewModel.selectedRemoteDeviceMUID }
InitiatorDestinationSelector(destinationMUID,
onChange = { Snapshot.withMutableSnapshot { ViewModel.selectedRemoteDeviceMUID.value = it } })

val conn = ViewModel.selectedRemoteDevice.value
if (conn != null) {
ClientConnection(conn)
Row {
val destinationMUID by remember { ViewModel.selectedRemoteDeviceMUID }
InitiatorDestinationSelector(destinationMUID,
onChange = { Snapshot.withMutableSnapshot { ViewModel.selectedRemoteDeviceMUID.value = it } })

if (conn != null)
ClientConnectionInfo(conn)
}
if (conn != null)
ClientConnection(conn)
}
}

@Composable
fun ClientConnection(vm: ConnectionViewModel) {
val conn = vm.conn
Column(Modifier.padding(12.dp, 0.dp)) {
Text("Device", fontSize = TextUnit(1.5f, TextUnitType.Em), fontWeight = FontWeight.Bold)
val small = TextUnit(0.8f, TextUnitType.Em)
Text("Manufacturer: ${conn.device.manufacturer.toString(16)}", fontSize = small)
Text("Family: ${conn.device.family.toString(16)}", fontSize = small)
Text("Model: ${conn.device.modelNumber.toString(16)}", fontSize = small)
Text("Revision: ${conn.device.softwareRevisionLevel.toString(16)}", fontSize = small)
Text("instance ID: ${conn.productInstanceId}", fontSize = small)
Text("max connections: ${conn.maxSimultaneousPropertyRequests}")

Text("Profiles", fontSize = TextUnit(1.5f, TextUnitType.Em), fontWeight = FontWeight.Bold)

Row {
Expand All @@ -74,6 +64,37 @@ fun ClientConnection(vm: ConnectionViewModel) {
}
}

@Composable
fun DeviceItemCard(label: String) {
Card { Text(label, modifier= Modifier.padding(6.dp, 0.dp)) }
}

@Composable
private fun ClientConnectionInfo(vm: ConnectionViewModel) {
val conn = vm.conn
Column {
Text("Device", fontSize = TextUnit(1.5f, TextUnitType.Em), fontWeight = FontWeight.Bold)
val small = TextUnit(0.8f, TextUnitType.Em)
Row {
DeviceItemCard("Manufacturer")
Text(conn.device.manufacturer.toString(16), fontSize = small)
DeviceItemCard("Family")
Text(conn.device.family.toString(16), fontSize = small)
DeviceItemCard("Model")
Text(conn.device.modelNumber.toString(16), fontSize = small)
DeviceItemCard("Revision")
Text(conn.device.softwareRevisionLevel.toString(16), fontSize = small)
}
Row {
DeviceItemCard("instance ID")
Text(conn.productInstanceId, fontSize = small)
DeviceItemCard("max connections")
Text("${conn.maxSimultaneousPropertyRequests}")

}
}
}

@Composable
private fun InitiatorDestinationSelector(destinationMUID: Int,
onChange: (Int) -> Unit) {
Expand Down Expand Up @@ -155,7 +176,10 @@ fun ClientPropertyList(vm: ConnectionViewModel) {
val properties = vm.properties.map { it.id }.distinct()
Snapshot.withMutableSnapshot {
properties.forEach {
ClientPropertyListEntry(it, vm.selectedProperty.value == it) { propertyId -> vm.selectProperty(propertyId) }
ClientPropertyListEntry(it, vm.selectedProperty.value == it) {
propertyId -> vm.selectProperty(propertyId)
AppModel.ciDeviceManager.initiator.sendGetPropertyDataRequest(vm.conn.muid, propertyId)
}
}
}
}
Expand All @@ -175,37 +199,58 @@ fun ClientPropertyListEntry(propertyId: String, isSelected: Boolean, selectedPro
@Composable
fun ClientPropertyDetails(vm: ConnectionViewModel, propertyId: String) {
Column {
Text("Property Metadata")

val entry = vm.properties.first { it.id == propertyId }
val def = vm.conn.propertyClient.getPropertyList()?.firstOrNull { it.resource == propertyId }
Text("resource: ${entry.id}")
if (def != null) {
Text("canGet: ${def.canGet}")
Text("canSet: ${def.canSet}")
Text("canSubscribe: ${def.canSubscribe}")
Text("requireResId: ${def.requireResId}")
Text("mimeType: ${def.mediaTypes.joinToString()}")
Text("encodings: ${def.encodings.joinToString()}")
if (def.schema != null)
Text("schema: ${Json.getUnescapedString(def.schema!!)}")
Text("canPaginate: ${def.canPaginate}")
Text("columns:")
def.columns.forEach {
if (it.property != null)
Text("Property ${it.property}: ${it.title}")
// could be "else if", but in case we want to see buggy column entry...
if (it.link != null)
Text("Link ${it.link}: ${it.title}")
val def = vm.conn.propertyClient.getPropertyList()?.firstOrNull { it.resource == entry.id }
ClientPropertyValueEditor(vm, def, entry)
if (def != null)
ClientPropertyMetadata(vm, def)
else
Text("(Metadata not available - not in ResourceList)")
}
}

@Composable
fun PropertyColumn(label: String, content: @Composable () -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
Card(modifier = Modifier.width(180.dp).padding(12.dp, 0.dp)) { Text(label, Modifier.padding(12.dp, 0.dp)) }
content()
}
}

@Composable
fun ClientPropertyMetadata(vm: ConnectionViewModel, def: PropertyResource) {
Column(Modifier.padding(12.dp)) {
Text("Property Metadata", fontWeight = FontWeight.Bold, fontSize = TextUnit(1.2f, TextUnitType.Em))

PropertyColumn("resource") { TextField(def.resource, {}, enabled = false) }
PropertyColumn("canGet") { Checkbox(def.canGet, {}, enabled = false) }
PropertyColumn("canSet") { TextField(def.canSet, {}, enabled = false) }
PropertyColumn("canSubscribe") { Checkbox(def.canSubscribe, {}, enabled = false) }
PropertyColumn("requireResId") { Checkbox(def.requireResId, {}, enabled = false) }
PropertyColumn("mediaTypes") { TextField(def.mediaTypes.joinToString("\n"), {}, enabled = false, minLines = 2) }
PropertyColumn("encodings") { TextField(def.encodings.joinToString("\n"), {}, enabled = false, minLines = 2) }
PropertyColumn("schema") { TextField(if (def.schema == null) "" else Json.getUnescapedString(def.schema!!), {}, enabled = false) }
PropertyColumn("canPaginate") { Checkbox(def.canPaginate, {}, enabled = false) }
PropertyColumn("columns") {
Column(Modifier.padding(12.dp)) {
def.columns.forEach {
if (it.property != null)
Text("Property ${it.property}: ${it.title}")
if (it.link != null)
Text("Link ${it.link}: ${it.title}")
}
}
}
/*
Row {
val enabled by remember { it.enabled }
Switch(checked = enabled, onCheckedChange = { newEnabled ->
AppModel.ciDeviceManager.initiator.setProfile(vm.conn.muid, it.address, it.profile, newEnabled)
})
Text("${it.address.toString(16)}: ${it.profile}")
}*/
}
}
}

@Composable
fun ClientPropertyValueEditor(vm: ConnectionViewModel, def: PropertyResource?, property: ObservablePropertyList.Entry) {
Text("Property Value", fontWeight = FontWeight.Bold, fontSize = TextUnit(1.2f, TextUnitType.Em))

val mediaType = vm.conn.propertyClient.getMediaTypeFor(property.replyHeader)
if (mediaType != null)
Text("mediaType: $mediaType")
if (mediaType == null || mediaType == CommonRulesKnownMimeTypes.APPLICATION_JSON)
TextField(Json.getUnescapedString(property.body.toByteArray().decodeToString()), {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ class ConnectionViewModel(val conn: MidiCIInitiator.Connection) {
.forEach { Snapshot.withMutableSnapshot { it.enabled.value = profile.enabled } }
}

conn.properties.propertyChanged.add { entry ->
val index = properties.indexOfFirst { it.id == entry.id }
if (index < 0)
properties.add(entry)
else {
properties.removeAt(index)
properties.add(index, entry)
}
}

conn.properties.propertiesCatalogUpdated.add {
properties.clear()
properties.addAll(conn.properties.entries)
Expand Down

0 comments on commit 4b4a913

Please sign in to comment.