Skip to content

Commit

Permalink
Implement entrybutton action to open an HMI state
Browse files Browse the repository at this point in the history
  • Loading branch information
hufman committed Aug 26, 2023
1 parent ca95409 commit 339660f
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ class BMWRemotingServerImpl(val client: BMWRemotingClient,
rhmiManager.addEventHandler(handle, client)
}

override fun rhmi_ackActionEvent(
handle: Int?,
actionId: Int?,
confirmId: Int?,
success: Boolean?
) {
val appId: String? = rhmiAppId
if (appId == null || handle == null || rhmiHandle != handle) {
throw BMWRemoting.IllegalArgumentException(-1, "Incorrect RHMI handle")
}
if (actionId == null || success == null) {
throw BMWRemoting.IllegalArgumentException(-1, "Missing parameters")
}
rhmiManager.ackActionEvent(appId, actionId, success)
}

override fun rhmi_setData(handle: Int?, modelId: Int?, value: Any?) {
if (handle == null || rhmiHandle != handle) {
throw BMWRemoting.IllegalArgumentException(-1, "Incorrect RHMI handle")
Expand Down
81 changes: 80 additions & 1 deletion android/src/main/kotlin/io/bimmergestalt/headunit/Pigeon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,51 @@ data class RHMIAppInfo (
)
}
}

@Suppress("UNCHECKED_CAST")
private object ServerApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
128.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
AMAppInfo.fromList(it)
}
}
129.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
RHMIAppInfo.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is AMAppInfo -> {
stream.write(128)
writeValue(stream, value.toList())
}
is RHMIAppInfo -> {
stream.write(129)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}

/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface ServerApi {
fun getPlatformVersion(): String
fun startServer()
fun amTrigger(appId: String)
fun rhmiAction(appId: String, actionId: Long, args: Map<Long, Any?>, callback: (Result<Boolean>) -> Unit)
fun rhmiEvent(appId: String, componentId: Long, eventId: Long, args: Map<Long, Any?>)

companion object {
/** The codec used by ServerApi. */
val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec()
ServerApiCodec
}
/** Sets up an instance of `ServerApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
Expand Down Expand Up @@ -164,6 +199,50 @@ interface ServerApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.headunit.ServerApi.rhmiAction", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val appIdArg = args[0] as String
val actionIdArg = args[1].let { if (it is Int) it.toLong() else it as Long }
val argsArg = args[2] as Map<Long, Any?>
api.rhmiAction(appIdArg, actionIdArg, argsArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.headunit.ServerApi.rhmiEvent", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val appIdArg = args[0] as String
val componentIdArg = args[1].let { if (it is Int) it.toLong() else it as Long }
val eventIdArg = args[2].let { if (it is Int) it.toLong() else it as Long }
val argsArg = args[3] as Map<Long, Any?>
var wrapped: List<Any?>
try {
api.rhmiEvent(appIdArg, componentIdArg, eventIdArg, argsArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.os.HandlerThread
import io.bimmergestalt.headunit.managers.AMManager
import io.bimmergestalt.headunit.managers.RHMIManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodChannel

import io.bimmergestalt.idriveconnectkit.RHMIDimensions

Expand Down Expand Up @@ -42,4 +41,16 @@ class ServerPlugin: FlutterPlugin, ServerApi {
amManager.onAppEvent(appId)
}
}

override fun rhmiAction(appId: String, actionId: Long, args: Map<Long, Any?>, callback: (Result<Boolean>) -> Unit) {
ioHandler.post {
rhmiManager.onActionEvent(appId, actionId.toInt(), args, callback)
}
}

override fun rhmiEvent(appId: String, componentId: Long, eventId: Long, args: Map<Long, Any?>) {
ioHandler.post {
rhmiManager.onHmiEvent(appId, componentId.toInt(), eventId.toInt(), args)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class RHMIManager(val callbacks: HeadunitCallbacks) {
private val knownApps = HashMap<String, RHMIAppInfo>()
private val actionHandlers = HashMap<Int, BMWRemotingClient>()
private val eventHandlers = HashMap<Int, BMWRemotingClient>()
private val actionCallbacks = HashMap<Int, HashMap<Int, (Result<Boolean>) -> Unit>>()

fun registerApp(handle: Int, appId: String, resources: Map<RHMIResourceType, ByteArray>) {
val existing = knownApps[appId]
Expand Down Expand Up @@ -94,4 +95,31 @@ class RHMIManager(val callbacks: HeadunitCallbacks) {
fun triggerEvent(appId: String, eventId: Int, args: Map<Int, Any?>) {
callbacks.rhmiTriggerEvent(appId, eventId, args)
}

fun onActionEvent(appId: String, actionId: Int, args: Map<*, *>, callback: (Result<Boolean>) -> Unit) {
val existing = knownApps[appId]
if (existing != null) {
actionHandlers[existing.handle.toInt()]?.rhmi_onActionEvent(existing.handle.toInt(), "", actionId, args)

if (!actionCallbacks.containsKey(existing.handle.toInt())) {
actionCallbacks[existing.handle.toInt()] = HashMap()
}
actionCallbacks[existing.handle.toInt()]?.put(actionId, callback)
}
}

fun onHmiEvent(appId: String, componentId: Int, eventId: Int, args: Map<*, *>) {
val existing = knownApps[appId]
if (existing != null) {
actionHandlers[existing.handle.toInt()]?.rhmi_onHmiEvent(existing.handle.toInt(), "", componentId, eventId, args)
}
}

fun ackActionEvent(appId: String, actionId: Int, success: Boolean) {
val existing = knownApps[appId]
if (existing != null) {
val callback = actionCallbacks[existing.handle.toInt()]?.remove(actionId)
callback?.invoke(Result.success(success))
}
}
}
3 changes: 2 additions & 1 deletion app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class _MyAppState extends State<MyApp> implements HeadunitApi {
this.entryButtonsByCategory.clear();
this.entryButtonsByCategory.addAll(entryButtonsByCategory);
}

@override
void amRegisterApp(AMAppInfo appInfo) {
setState(() {
Expand Down Expand Up @@ -125,7 +126,7 @@ class _MyAppState extends State<MyApp> implements HeadunitApi {

@override
void rhmiSetData(String appId, int modelId, Object? value) {
// TODO: implement rhmiSetData
rhmiApps[appId]?.description.setData(modelId, value);
}

@override
Expand Down
115 changes: 108 additions & 7 deletions app/lib/rhmi.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class RHMIApp {

class RHMIAppDescription {
Map<String, RHMIComponent> entryButtons = {};
Map<int, RHMIAction> actions = {};
Map<int, RHMIModel> models = {};
Map<int, RHMIComponent> components = {};
Map<int, RHMIState> states = {};
Expand All @@ -79,14 +80,34 @@ class RHMIAppDescription {
final app = RHMIAppDescription();
final description = XmlDocument.parse(data);
description.getElement('pluginApps')?.findElements('pluginApp').forEach((pluginApp) {
// TODO actions
pluginApp.getElement('models')?.childElements.forEach((node) {
final parsed = RHMIModel.loadXml(node);
if (parsed.id > 0) {
app.models[parsed.id] = parsed;
}
});

pluginApp.getElement('actions')?.childElements.forEach((node) {
final parsed = RHMIAction.loadXml(node);
if (parsed.id > 0) {
app.actions[parsed.id] = parsed;
}
if (parsed is RHMIHmiAction) {
parsed.linkModel(app.models);
}
if (parsed is RHMICombinedAction) {
final hmiAction = parsed.hmiAction;
if (hmiAction != null) {
app.actions[hmiAction.id] = hmiAction;
hmiAction.linkModel(app.models);
}
final raAction = parsed.raAction;
if (raAction != null) {
app.actions[raAction.id] = raAction;
}
}
});

pluginApp.getElement('hmiStates')?.childElements.forEach((node) {
final parsed = RHMIState.loadXml(app, node);
if (parsed.id > 0) {
Expand All @@ -104,31 +125,109 @@ class RHMIAppDescription {
return app;
}

void setData(int model, Object value) {
void setData(int model, Object? value) {
if (!models.containsKey(model)) {
throw Exception("Unknown model $model");
log("Unknown model $model from ${models.keys}");
}
log("Setting data $model to $value");
models[model]?.value = value;
}
}

class RHMIAction {
RHMIAction(this.id, this.type, this.attributes);
int id;
String type;

Map<String, String?> attributes = {};

static RHMIAction loadXml(XmlElement node) {
final idNode = node.attributes.firstWhere((p0) => p0.name.local == 'id',
orElse: (() => XmlAttribute(XmlName.fromString(""), "-1")));
final id = int.parse(idNode.value);
final type = node.localName;
final attributeNodes = node.attributes.where((p0) => p0.name.local != 'id');
final attributes = {for (var attr in attributeNodes) attr.name.local: attr.value};

final action = switch (type) {
"combinedAction" => RHMICombinedAction(id, type, attributes),
"hmiAction" => RHMIHmiAction(id, type, attributes),
_ => RHMIAction(id, type, attributes),
};
if (action is RHMICombinedAction) {
action.loadChildren(node);
}
return action;
}

static Map<String, RHMIAction> loadReferencedActions(RHMIAppDescription app, Map<String, Object> attributes) {
final Map<String, RHMIAction> actions = {};
attributes.forEach((attrName, attrValue) {
final derefAction = app.actions[attrValue];
if (attrName.toLowerCase().endsWith("action") && derefAction != null) {
actions[attrName] = derefAction;
}
});
return actions;
}
}

class RHMICombinedAction extends RHMIAction {
RHMICombinedAction(super.id, super.type, super.attributes);

RHMIAction? raAction;
RHMIHmiAction? hmiAction;

void loadChildren(XmlNode combinedActionNode) {
combinedActionNode.getElement("actions")?.childElements.forEach((element) {
final action = RHMIAction.loadXml(element);
if (action.type == "raAction") {
raAction = action;
}
if (action is RHMIHmiAction && action.type == "hmiAction") {
hmiAction = action;
}
});
}

void linkModel(Map<int, RHMIModel> models) {
hmiAction?.linkModel(models);
}
}

class RHMIHmiAction extends RHMIAction {
RHMIHmiAction(super.id, super.type, super.attributes):
target = int.tryParse(attributes['target'] ?? '', radix: 10),
targetModelId = int.tryParse(attributes['targetModel'] ?? '', radix: 10);

int? target;
int? targetModelId;
RHMIModel? targetModel;

void linkModel(Map<int, RHMIModel> models) {
if (targetModelId != null) {
targetModel = models[targetModelId];
}
}
}

class RHMIModel {
RHMIModel(this.id, this.type);
int id;
String type;
Object? value;
Map<String, Object?> properties = {};
Map<String, Object?> attributes = {};

static RHMIModel loadXml(XmlElement node) {
final idNode = node.attributes.firstWhere((p0) => p0.name.local == 'id',
orElse: (() => XmlAttribute(XmlName.fromString(""), "-1")));
final id = int.parse(idNode.value);
final type = node.localName;
final propertyNodes = node.attributes.where((p0) => p0.name.local != 'id');
final properties = {for (var attr in propertyNodes) attr.name.local: int.tryParse(attr.value, radix: 10) ?? attr.value};
final attributeNodes = node.attributes.where((p0) => p0.name.local != 'id');
final attributes = {for (var attr in attributeNodes) attr.name.local: int.tryParse(attr.value, radix: 10) ?? attr.value};

final model = RHMIModel(id, type);
model.properties.addAll(properties);
model.attributes.addAll(attributes);
return model;
}

Expand Down Expand Up @@ -179,6 +278,7 @@ class RHMIComponent {

int id;
String type;
Map<String, RHMIAction> actions = {};
Map<String, RHMIModel> models = {};
Map<int, String> properties = {};

Expand All @@ -192,6 +292,7 @@ class RHMIComponent {

final component = RHMIComponent(id, type);
// TODO action
component.actions.addAll(RHMIAction.loadReferencedActions(app, attributes));
component.models.addAll(RHMIModel.loadReferencedModels(app, attributes));
component.properties.addAll(RHMIProperty.loadProperties(node.getElement('properties')));
return component;
Expand Down
Loading

0 comments on commit 339660f

Please sign in to comment.