Add vhal replay for car data
Add 2 buttons to the car data page to:
Import Json file
Send emulator event according to the json
Todo: Improve UI
Add Pause
Add json format check when format is decided
Hide the feature behand CarVhalReplay flag
Test: emulator -feature CarVhalReplay
Bug: 150640811
Change-Id: Id38f389ebf9659513651fe66c42048cef3837b31
diff --git a/android/android-emu/android/featurecontrol/FeatureControlDefHost.h b/android/android-emu/android/featurecontrol/FeatureControlDefHost.h
index 2edb265..f4e7c5a 100644
--- a/android/android-emu/android/featurecontrol/FeatureControlDefHost.h
+++ b/android/android-emu/android/featurecontrol/FeatureControlDefHost.h
@@ -55,3 +55,4 @@
FEATURE_CONTROL_ITEM(CarVHalTable)
FEATURE_CONTROL_ITEM(VulkanSnapshots)
FEATURE_CONTROL_ITEM(DynamicMediaProfile)
+FEATURE_CONTROL_ITEM(CarVhalReplay)
diff --git a/android/android-emu/android/featurecontrol/proto/emulator_features.proto b/android/android-emu/android/featurecontrol/proto/emulator_features.proto
index 319b689..a50e5d1 100644
--- a/android/android-emu/android/featurecontrol/proto/emulator_features.proto
+++ b/android/android-emu/android/featurecontrol/proto/emulator_features.proto
@@ -89,4 +89,6 @@
Mac80211hwsimUserspaceManaged = 50;
HasSharedSlotsHostMemoryAllocator = 51;
+
+ CarVhalReplay = 52;
}
diff --git a/android/android-emu/android/metrics/metrics.cpp b/android/android-emu/android/metrics/metrics.cpp
index 337a9b3..20f7fb3 100644
--- a/android/android-emu/android/metrics/metrics.cpp
+++ b/android/android-emu/android/metrics/metrics.cpp
@@ -435,6 +435,8 @@
return android_studio::EmulatorFeatureFlagState::MAC80211HWSIM_USERSPACE_MANAGED;
case android::featurecontrol::HasSharedSlotsHostMemoryAllocator:
return android_studio::EmulatorFeatureFlagState::HAS_SHARED_SLOTS_HOST_MEMORY_ALLOCATOR;
+ case android::featurecontrol::CarVhalReplay:
+ return android_studio::EmulatorFeatureFlagState::CAR_VHAL_REPLAY;
}
return android_studio::EmulatorFeatureFlagState::EMULATOR_FEATURE_FLAG_UNSPECIFIED;
}
diff --git a/android/android-emu/android/metrics/proto/studio_stats.proto b/android/android-emu/android/metrics/proto/studio_stats.proto
index 046a6e4..ae3c0db 100755
--- a/android/android-emu/android/metrics/proto/studio_stats.proto
+++ b/android/android-emu/android/metrics/proto/studio_stats.proto
@@ -1426,7 +1426,8 @@
VIRTIO_GPU_NEXT = 55;
MAC80211HWSIM_USERSPACE_MANAGED = 56;
HAS_SHARED_SLOTS_HOST_MEMORY_ALLOCATOR = 57;
- // Next tag: 58
+ CAR_VHAL_REPLAY = 58;
+ // Next tag: 59
}
// Which features were enabled by default or through the server-side config.
repeated EmulatorFeatureFlag attempted_enabled_feature_flags = 1;
diff --git a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp
index 61137a6..6929c21 100644
--- a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp
+++ b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.cpp
@@ -10,17 +10,27 @@
// GNU General Public License for more details.
#include "android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h"
-#include <stdint.h> // for int32_t
-#include <QCheckBox> // for QCheckBox
-#include <QComboBox> // for QComboBox
-#include <QLabel> // for QLabel
-#include <QSlider> // for QSlider
-#include <utility> // for move
+#include <stdint.h> // for int32_t
-#include "android/emulation/proto/VehicleHalProto.pb.h" // for EmulatorMessage
-#include "android/utils/debug.h" // for VERBOSE_PRINT
-#include "ui_car-sensor-data.h" // for CarSensorData
-#include "vehicle_constants_generated.h" // for VehicleIgnit...
+#include <QByteArray> // for QByteArray
+#include <QCheckBox> // for QCheckBox
+#include <QComboBox> // for QComboBox
+#include <QFileDialog> // for QFileDialog
+#include <QJsonArray> // for QJsonArray
+#include <QJsonDocument> // for QJsonDocument
+#include <QJsonObject> // for QJsonObject
+#include <QJsonValue> // for QJsonValue
+#include <QLabel> // for QLabel
+#include <QSlider> // for QSlider
+#include <QTextStream> // for QSlider
+#include <utility> // for move
+
+#include "android/base/Log.h"
+#include "android/featurecontrol/feature_control.h"
+#include "android/skin/qt/error-dialog.h" // for showErrorDialog
+#include "android/utils/debug.h" // for VERBOSE_PRINT
+#include "ui_car-sensor-data.h" // for CarSensorData
+#include "vehicle_constants_generated.h" // for VehicleIgnit...
class QWidget;
@@ -31,14 +41,26 @@
using emulator::EmulatorMessage;
using emulator::MsgType;
using emulator::Status;
-using emulator::VehicleProperty;
-using emulator::VehiclePropValue;
+using emulator::TimedEmulatorMessages;
using emulator::VehicleGear;
using emulator::VehicleIgnitionState;
+using emulator::VehicleProperty;
+using emulator::VehiclePropValue;
+using emulator::VhalEventLoaderThread;
+
+static constexpr int64_t VHAL_REPLAY_INTERVAL = 1000;
CarSensorData::CarSensorData(QWidget* parent)
: QWidget(parent), mUi(new Ui::CarSensorData) {
mUi->setupUi(this);
+
+ if (!feature_is_enabled(kFeature_CarVhalReplay)) {
+ mUi->button_loadrecord->setVisible(false);
+ mUi->button_playrecord->setVisible(false);
+ QObject::connect(&mTimer, &QTimer::timeout, this,
+ &CarSensorData::VhalTimeout);
+ prepareVhalLoader();
+ }
}
static const enum VehicleGear sComboBoxGearValues[] = {
@@ -57,6 +79,21 @@
return emulatorMsg;
}
+void CarSensorData::VhalTimeout() {
+ if (mTimedEmulatorMessages.getStatus() != TimedEmulatorMessages::START) {
+ mTimer.stop();
+ return;
+ }
+ std::vector<emulator::EmulatorMessage> events =
+ mTimedEmulatorMessages.getEvents(VHAL_REPLAY_INTERVAL);
+ int realIndex = mTimedEmulatorMessages.getCurrentIndex() - events.size();
+ for (auto event : events) {
+ string log = "Send event from json index: " + std::to_string(realIndex);
+ realIndex++;
+ mSendEmulatorMsg(event, log);
+ }
+}
+
void CarSensorData::sendGearChangeMsg(const int gear, const string& gearName) {
// TODO: Grey out the buttons when callback is not set or vehicle hal is
// not connected.
@@ -148,6 +185,40 @@
mUi->comboBox_gear->currentText().toStdString());
}
+void CarSensorData::on_button_loadrecord_clicked() {
+ prepareVhalLoader();
+
+ QString fileName = QFileDialog::getOpenFileName(
+ this, tr("Open Json File"), ".", tr("Json files (*.json)"));
+
+ if (fileName.isNull())
+ return;
+ parseEventsFromJsonFile(fileName);
+}
+
+void CarSensorData::prepareVhalLoader() {
+ mVhalEventLoader.reset(VhalEventLoaderThread::newInstance());
+
+ connect(mVhalEventLoader.get(), &VhalEventLoaderThread::started, this,
+ &CarSensorData::vhalEventThreadStarted);
+
+ connect(mVhalEventLoader.get(), &VhalEventLoaderThread::loadingFinished,
+ this, &CarSensorData::startupVhalEventThreadFinished);
+
+ // Make sure new_instance gets cleaned up after the thread exits.
+ connect(mVhalEventLoader.get(), &VhalEventLoaderThread::finished,
+ mVhalEventLoader.get(), &QObject::deleteLater);
+}
+
+void CarSensorData::on_button_playrecord_clicked() {
+ mTimedEmulatorMessages.setStatus(TimedEmulatorMessages::START);
+ mTimer.start(VHAL_REPLAY_INTERVAL);
+}
+
+void CarSensorData::parseEventsFromJsonFile(QString jsonPath) {
+ mVhalEventLoader->loadVhalEventFromFile(jsonPath, &mTimedEmulatorMessages);
+}
+
void CarSensorData::processMsg(emulator::EmulatorMessage emulatorMsg) {
if (emulatorMsg.prop_size() == 0 && emulatorMsg.value_size() == 0) {
return;
@@ -226,3 +297,226 @@
}
return len - 1;
}
+
+void CarSensorData::vhalEventThreadStarted() {
+ // Prevent the user from initiating a load json while another load is
+ // already in progress
+ mUi->button_loadrecord->setEnabled(false);
+ mUi->button_playrecord->setEnabled(false);
+
+ mNowLoadingVhalEvent = true;
+}
+
+void CarSensorData::startupVhalEventThreadFinished(QString file_name,
+ bool ok,
+ QString error_message) {
+ // on startup, we silently ignore the previously remebered event data file
+ // being missing or malformed.
+ finishVhalEventLoading(file_name, ok, error_message, true);
+}
+
+void CarSensorData::vhalEventThreadFinished(QString file_name,
+ bool ok,
+ QString error_message) {
+ // on startup, we silently ignore the previously remebered event data file
+ // being missing or malformed.
+ finishVhalEventLoading(file_name, ok, error_message, false);
+}
+
+void CarSensorData::finishVhalEventLoading(const QString& file_name,
+ bool ok,
+ const QString& error_message,
+ bool ignore_error) {
+ mVhalEventLoader.reset();
+ updateControlsAfterLoading();
+
+ if (!ok) {
+ if (!ignore_error) {
+ showErrorDialog(error_message, tr("Vhal EVENT Parser"));
+ }
+ return;
+ }
+}
+
+void CarSensorData::updateControlsAfterLoading() {
+ mUi->button_loadrecord->setEnabled(true);
+ mUi->button_playrecord->setEnabled(true);
+
+ mNowLoadingVhalEvent = false;
+}
+
+void VhalEventLoaderThread::loadVhalEventFromFile(
+ const QString& file_name,
+ TimedEmulatorMessages* events) {
+ mFileName = file_name;
+ mTimedEmulatorMessages = events;
+ start();
+}
+
+void VhalEventLoaderThread::run() {
+ if (mFileName.isEmpty() || mTimedEmulatorMessages == nullptr) {
+ emit(loadingFinished(mFileName, false, tr("No file to load")));
+ return;
+ }
+ bool ok = false;
+ std::string err_str;
+
+ QFileInfo file_info(mFileName);
+ mTimedEmulatorMessages->clear();
+ auto suffix = file_info.suffix().toLower();
+ if (suffix == "json") {
+ ok = parseJsonFile(mFileName.toStdString().c_str(),
+ mTimedEmulatorMessages);
+ } else {
+ err_str = tr("Unknown file type").toStdString();
+ }
+
+ auto err_qstring = QString::fromStdString(err_str);
+ emit(loadingFinished(mFileName, ok, err_qstring));
+}
+
+bool VhalEventLoaderThread::parseJsonFile(
+ const char* filePath,
+ TimedEmulatorMessages* timedEmulatorMessages) {
+ QString jsonString;
+ if (filePath) {
+ jsonString = readJsonStringFromFile(filePath);
+ }
+
+ QJsonDocument eventDoc = QJsonDocument::fromJson(jsonString.toUtf8());
+ if (eventDoc.isNull()) {
+ return false;
+ } else {
+ return loadEmulatorEvents(eventDoc, timedEmulatorMessages);
+ }
+ return false;
+}
+
+bool VhalEventLoaderThread::loadEmulatorEvents(
+ const QJsonDocument& eventDoc,
+ TimedEmulatorMessages* timedEmulatorMessages) {
+ if (eventDoc.isNull()) {
+ return false;
+ }
+ QJsonObject eventsJson = eventDoc.object();
+ QJsonArray eventArray = eventsJson.value("events").toArray();
+
+ for (int eventIdx = 0; eventIdx < eventArray.size(); eventIdx++) {
+ QJsonObject eventObject = eventArray.at(eventIdx).toObject();
+ QJsonDocument Doc(eventObject);
+ QByteArray ba = Doc.toJson();
+ QJsonObject sensorrecord =
+ eventObject.value("sensor_records").toObject();
+ QJsonObject carPropertyValues =
+ sensorrecord.value("car_property_values").toObject();
+ int propID = carPropertyValues.value("key").toInt();
+
+ EmulatorMessage emulatorMsg = makeSetPropMsg();
+ VehiclePropValue* value = emulatorMsg.add_value();
+ value->set_prop(static_cast<int32_t>(propID));
+
+ int prop = carPropertyValues.value("key").toInt();
+ int areaId = carPropertyValues.value("value")
+ .toObject()
+ .value("area_id")
+ .toInt();
+ QJsonObject propertyValue = carPropertyValues.value("value").toObject();
+ int type = 0;
+ if (propertyValue.contains("int32_values")) {
+ value->add_int32_values(
+ propertyValue.value("int32_values").toInt());
+ } else if (propertyValue.contains("float_values")) {
+ value->add_float_values(
+ (float)propertyValue.value("float_values").toDouble());
+ }
+
+ char* error = nullptr;
+ timedEmulatorMessages->addEvents(
+ strtol(sensorrecord.value("timestamp_ns")
+ .toString()
+ .toStdString()
+ .c_str(),
+ &error, 0),
+ emulatorMsg);
+ }
+
+ return true;
+}
+
+QString VhalEventLoaderThread::readJsonStringFromFile(const char* filePath) {
+ QString fullContents;
+ if (filePath) {
+ QFile jsonFile(filePath);
+ if (jsonFile.open(QFile::ReadOnly | QFile::Text)) {
+ QTextStream jsonStream(&jsonFile);
+ fullContents = jsonStream.readAll();
+ jsonFile.close();
+ }
+ }
+ return fullContents;
+}
+
+VhalEventLoaderThread* VhalEventLoaderThread::newInstance() {
+ VhalEventLoaderThread* new_instance = new VhalEventLoaderThread();
+ return new_instance;
+}
+
+std::vector<emulator::EmulatorMessage> TimedEmulatorMessages::getEvents(
+ int64_t interval) {
+ std::vector<emulator::EmulatorMessage> res;
+
+ if (mTimestampes.size() != mEmulatorMessages.size()) {
+ clear();
+ return res;
+ }
+ if (mCurrIndex == mEmulatorMessages.size()) {
+ mCurrIndex = 0;
+ mBaseTimeStamp = -1;
+ mStatus = STOP;
+ return res;
+ }
+
+ int64_t nextTimeStamp = mBaseTimeStamp + (int64_t)interval;
+
+ for (; mCurrIndex < mEmulatorMessages.size(); mCurrIndex++) {
+ int64_t timestamp = mTimestampes.at(mCurrIndex);
+ if (timestamp >= mBaseTimeStamp && timestamp < nextTimeStamp) {
+ res.push_back(mEmulatorMessages.at(mCurrIndex));
+ } else {
+ break;
+ }
+ }
+ mBaseTimeStamp = nextTimeStamp;
+ return res;
+}
+
+void TimedEmulatorMessages::addEvents(int64_t timestamp,
+ emulator::EmulatorMessage& msg) {
+ if (mBaseTimeStamp == -1) {
+ mBaseTimeStamp = timestamp;
+ }
+ if (mTimestampes.size() != mEmulatorMessages.size()) {
+ return;
+ }
+ mEmulatorMessages.push_back(msg);
+ mTimestampes.push_back(timestamp);
+}
+
+void TimedEmulatorMessages::clear() {
+ mEmulatorMessages.clear();
+ mTimestampes.clear();
+ mCurrIndex = 0;
+ mBaseTimeStamp = -1;
+}
+
+TimedEmulatorMessages::PlayStatus TimedEmulatorMessages::getStatus() {
+ return mStatus;
+}
+
+void TimedEmulatorMessages::setStatus(PlayStatus status) {
+ mStatus = status;
+}
+
+int TimedEmulatorMessages::getCurrentIndex() {
+ return mCurrIndex;
+}
\ No newline at end of file
diff --git a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h
index e89cd43..5734f74 100644
--- a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h
+++ b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.h
@@ -10,19 +10,74 @@
// GNU General Public License for more details.
#pragma once
-#include <qobjectdefs.h> // for Q_OBJECT, slots
-#include <QString> // for QString
-#include <QWidget> // for QWidget
-#include <functional> // for function
-#include <memory> // for unique_ptr
-#include <string> // for string
+// TODO: (b/120444474) rename ERROR_INVALID_OPERATION & remove this undef
+#undef ERROR_INVALID_OPERATION
-#include "ui_car-sensor-data.h" // for CarSensorData
+#include <qobjectdefs.h> // for Q_OBJECT, slots
+
+#include <QString> // for QString
+#include <QThread> // for QThread
+#include <QTimer> // for QTimer
+#include <QWidget> // for QWidget
+#include <functional> // for function
+#include <memory> // for unique_ptr
+#include <string> // for string
+
+#include "android/emulation/proto/VehicleHalProto.pb.h" // for EmulatorMessage
+#include "ui_car-sensor-data.h" // for CarSensorData
class QObject;
class QWidget;
namespace emulator {
+
+class TimedEmulatorMessages {
+public:
+ enum PlayStatus { STOP, START, PAUSE };
+
+ std::vector<emulator::EmulatorMessage> getEvents(int64_t interval);
+ void addEvents(int64_t timestamp, emulator::EmulatorMessage& msg);
+ void clear();
+ PlayStatus getStatus();
+ void setStatus(PlayStatus status);
+ int getCurrentIndex();
+
+private:
+ std::vector<emulator::EmulatorMessage> mEmulatorMessages;
+ std::vector<int64_t> mTimestampes;
+ int mCurrIndex = 0;
+ int64_t mBaseTimeStamp = -1;
+ PlayStatus mStatus;
+};
+
+class VhalEventLoaderThread : public QThread {
+ Q_OBJECT
+public:
+ // Loads Vhal from a json file specified
+ // by file_name into the EmulatorMessage array
+ void loadVhalEventFromFile(const QString& file_name,
+ emulator::TimedEmulatorMessages* events);
+
+ static VhalEventLoaderThread* newInstance();
+
+signals:
+ void loadingFinished(QString file_name, bool ok, QString error);
+
+protected:
+ // Reimplemented to load the file into the given fixes array.
+ void run() override;
+
+private:
+ VhalEventLoaderThread() = default;
+ QString mFileName;
+ emulator::TimedEmulatorMessages* mTimedEmulatorMessages = nullptr;
+ bool parseJsonFile(const char* filePath,
+ emulator::TimedEmulatorMessages* emulatorMessages);
+ QString readJsonStringFromFile(const char* filePath);
+ bool loadEmulatorEvents(const QJsonDocument& eventDoc,
+ emulator::TimedEmulatorMessages* emulatorMessages);
+};
+
class EmulatorMessage;
}
class CarSensorData : public QWidget {
@@ -42,6 +97,12 @@
void on_checkBox_night_toggled();
void on_checkBox_park_toggled();
void on_checkBox_fuel_low_toggled();
+ void on_button_loadrecord_clicked();
+ void on_button_playrecord_clicked();
+ void vhalEventThreadStarted();
+ void startupVhalEventThreadFinished(QString file_name,
+ bool ok,
+ QString error);
private:
std::unique_ptr<Ui::CarSensorData> mUi;
@@ -57,4 +118,22 @@
const std::string& ignitionName);
float getSpeedMetersPerSecond(int speed, int unitIndex);
int getIndexFromVehicleGear(int gear);
-};
+ void parseEventsFromJsonFile(QString jsonPath);
+
+ // Vhal replay
+ std::unique_ptr<emulator::VhalEventLoaderThread> mVhalEventLoader;
+ emulator::TimedEmulatorMessages mTimedEmulatorMessages;
+ bool mNowLoadingVhalEvent = false;
+ QTimer mTimer;
+
+ void VhalTimeout();
+ void vhalEventThreadFinished(QString file_name,
+ bool ok,
+ QString error_message);
+ void updateControlsAfterLoading();
+ void finishVhalEventLoading(const QString& file_name,
+ bool ok,
+ const QString& error_message,
+ bool ignore_error);
+ void prepareVhalLoader();
+};
\ No newline at end of file
diff --git a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui
index eeea38a..a68b4e2 100644
--- a/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui
+++ b/android/android-emu/android/skin/qt/extended-pages/car-data-emulation/car-sensor-data.ui
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
- <width>621</width>
+ <width>460</width>
<height>456</height>
</rect>
</property>
@@ -399,6 +399,32 @@
<string>Title</string>
</property>
</widget>
+ <widget class="QPushButton" name="button_loadrecord">
+ <property name="geometry">
+ <rect>
+ <x>20</x>
+ <y>210</y>
+ <width>95</width>
+ <height>26</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>LOAD RECORD</string>
+ </property>
+ </widget>
+ <widget class="QPushButton" name="button_playrecord">
+ <property name="geometry">
+ <rect>
+ <x>20</x>
+ <y>250</y>
+ <width>95</width>
+ <height>26</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>PLAY RECORD</string>
+ </property>
+ </widget>
</widget>
<resources/>
<connections/>
diff --git a/android/data/advancedFeatures.ini b/android/data/advancedFeatures.ini
index 96e33f0c..3b0e92c 100644
--- a/android/data/advancedFeatures.ini
+++ b/android/data/advancedFeatures.ini
@@ -304,3 +304,7 @@
# HasSharedSlotsHostMemoryAllocator---------------------------------------------
# Host supports AddressSpaceSharedSlotsHostMemoryAllocatorContext
HasSharedSlotsHostMemoryAllocator = on
+
+# CarVHalReplay--------------------------------------------------------------
+# if enabled, Car Vhal Load and Play button will show in extended window -> CarData
+CarVhalReplay = off
\ No newline at end of file
diff --git a/android/data/advancedFeaturesCanary.ini b/android/data/advancedFeaturesCanary.ini
index 2ca7c84..096062d 100644
--- a/android/data/advancedFeaturesCanary.ini
+++ b/android/data/advancedFeaturesCanary.ini
@@ -309,3 +309,7 @@
# HasSharedSlotsHostMemoryAllocator---------------------------------------------
# Host supports AddressSpaceSharedSlotsHostMemoryAllocatorContext
HasSharedSlotsHostMemoryAllocator = on
+
+# CarVHalReplay--------------------------------------------------------------
+# if enabled, Car Vhal Load and Play button will show in extended window -> CarData
+CarVhalReplay = off
\ No newline at end of file