Merge changes from topic "Cuttlefish-sensor-injection" into main
* changes:
Implement Cuttlefish motion sensors UI
Inject Accelerometer and magnetometer values.
diff --git a/build/Android.bp b/build/Android.bp
index d517ac4..0f0a660 100644
--- a/build/Android.bp
+++ b/build/Android.bp
@@ -164,6 +164,7 @@
"webrtc_index.css",
"webrtc_controls.css",
"webrtc_trusted.pem",
+ "webrtc_sensors.js",
]
cvd_host_model_simulator_files = [
diff --git a/guest/commands/sensor_injection/main.cpp b/guest/commands/sensor_injection/main.cpp
index fe91387..ccef057 100644
--- a/guest/commands/sensor_injection/main.cpp
+++ b/guest/commands/sensor_injection/main.cpp
@@ -20,9 +20,9 @@
#include <android-base/chrono_utils.h>
#include <android-base/logging.h>
#include <android/binder_manager.h>
+#include <android-base/parsedouble.h>
#include <android-base/parseint.h>
#include <utils/SystemClock.h>
-
#include <aidl/android/hardware/sensors/BnSensors.h>
using aidl::android::hardware::sensors::Event;
@@ -106,6 +106,66 @@
endSensorInjection(sensors);
}
+// Inject accelerometer event based on rotation in device position.
+void InjectAccelerometer(double x, double y, double z) {
+ auto sensors = startSensorInjection();
+ int handle = getSensorHandle(SensorType::ACCELEROMETER, sensors);
+ Event event;
+ event.sensorHandle = handle;
+ event.sensorType = SensorType::ACCELEROMETER;
+
+ Event::EventPayload::Vec3 vec3;
+ vec3.x = x;
+ vec3.y = y;
+ vec3.z = z;
+ vec3.status = SensorStatus::ACCURACY_HIGH;
+ event.payload.set<Event::EventPayload::Tag::vec3>(vec3);
+ event.timestamp = android::elapsedRealtimeNano();
+ auto result = sensors->injectSensorData(event);
+ CHECK(result.isOk()) << "Unable to inject ISensors accelerometer event: "
+ << result.getDescription();
+}
+
+// Inject Magnetometer event based on rotation in device position.
+void InjectMagnetometer(double x, double y, double z) {
+ auto sensors = startSensorInjection();
+ int handle = getSensorHandle(SensorType::MAGNETIC_FIELD, sensors);
+ Event event;
+ event.sensorHandle = handle;
+ event.sensorType = SensorType::MAGNETIC_FIELD;
+
+ Event::EventPayload::Vec3 vec3;
+ vec3.x = x;
+ vec3.y = y;
+ vec3.z = z;
+ vec3.status = SensorStatus::ACCURACY_HIGH;
+ event.payload.set<Event::EventPayload::Tag::vec3>(vec3);
+ event.timestamp = android::elapsedRealtimeNano();
+ auto result = sensors->injectSensorData(event);
+ CHECK(result.isOk()) << "Unable to inject ISensors magnetometer event: "
+ << result.getDescription();
+}
+
+// Inject Gyroscope event based on rotation in device position.
+void InjectGyroscope(double x, double y, double z){
+ auto sensors = startSensorInjection();
+ int handle = getSensorHandle(SensorType::GYROSCOPE, sensors);
+ Event event;
+ event.sensorHandle = handle;
+ event.sensorType = SensorType::GYROSCOPE;
+
+ Event::EventPayload::Vec3 vec3;
+ vec3.x = x;
+ vec3.y = y;
+ vec3.z = z;
+ vec3.status = SensorStatus::ACCURACY_HIGH;
+ event.payload.set<Event::EventPayload::Tag::vec3>(vec3);
+ event.timestamp = android::elapsedRealtimeNano();
+ auto result = sensors->injectSensorData(event);
+ CHECK(result.isOk()) << "Unable to inject ISensors gyroscope event: "
+ << result.getDescription();
+}
+
// Inject a single HINGE_ANGLE event at the given angle.
void InjectHingeAngle(int angle) {
auto sensors = startSensorInjection();
@@ -126,11 +186,11 @@
}
int main(int argc, char** argv) {
- ::android::base::InitLogging(argv, android::base::LogdLogger(android::base::SYSTEM));
-
- CHECK(argc == 3)
- << "Expected command line args 'rotate <angle>' or 'hinge_angle <value>'";
-
+ ::android::base::InitLogging(
+ argv, android::base::LogdLogger(android::base::SYSTEM));
+ CHECK(argc == 3 || argc == 11)
+ << "Expected command line args 'rotate <angle>', 'hinge_angle <value>', or 'motion " <<
+ "<acc_x> <acc_y> <acc_z> <mgn_x> <mgn_y> <mgn_z> <gyro_x> <gyro_y> <gyro_z>'";
if (!strcmp(argv[1], "rotate")) {
int rotationDeg;
CHECK(android::base::ParseInt(argv[2], &rotationDeg))
@@ -142,6 +202,20 @@
<< "Hinge angle must be an integer";
CHECK(angle >= 0 && angle <= 360) << "Bad hinge_angle value: " << argv[2];
InjectHingeAngle(angle);
+ } else if (!strcmp(argv[1], "motion")) {
+ double acc_x, acc_y, acc_z, mgn_x, mgn_y, mgn_z, gyro_x, gyro_y, gyro_z;
+ CHECK(android::base::ParseDouble(argv[2], &acc_x)) << "Accelerometer x value must be a double";
+ CHECK(android::base::ParseDouble(argv[3], &acc_y)) << "Accelerometer x value must be a double";
+ CHECK(android::base::ParseDouble(argv[4], &acc_z)) << "Accelerometer x value must be a double";
+ CHECK(android::base::ParseDouble(argv[5], &mgn_x)) << "Magnetometer x value must be a double";
+ CHECK(android::base::ParseDouble(argv[6], &mgn_y)) << "Magnetometer y value must be a double";
+ CHECK(android::base::ParseDouble(argv[7], &mgn_z)) << "Magnetometer z value must be a double";
+ CHECK(android::base::ParseDouble(argv[8], &gyro_x)) << "Gyroscope x value must be a double";
+ CHECK(android::base::ParseDouble(argv[9], &gyro_y)) << "Gyroscope y value must be a double";
+ CHECK(android::base::ParseDouble(argv[10], &gyro_z)) << "Gyroscope z value must be a double";
+ InjectAccelerometer(acc_x, acc_y, acc_z);
+ InjectMagnetometer(mgn_x, mgn_y, mgn_z);
+ InjectGyroscope(gyro_x, gyro_y, gyro_z);
} else {
LOG(FATAL) << "Unknown arg: " << argv[1];
}
diff --git a/host/frontend/webrtc/html_client/Android.bp b/host/frontend/webrtc/html_client/Android.bp
index 2476f15..a7f6c7b 100644
--- a/host/frontend/webrtc/html_client/Android.bp
+++ b/host/frontend/webrtc/html_client/Android.bp
@@ -80,3 +80,9 @@
sub_dir: "webrtc/assets/js",
}
+prebuilt_usr_share_host {
+ name: "webrtc_sensors.js",
+ src: "js/sensors.js",
+ filename: "sensors.js",
+ sub_dir: "webrtc/assets/js",
+}
diff --git a/host/frontend/webrtc/html_client/client.html b/host/frontend/webrtc/html_client/client.html
index 2995a5a5..e51049c 100644
--- a/host/frontend/webrtc/html_client/client.html
+++ b/host/frontend/webrtc/html_client/client.html
@@ -51,6 +51,7 @@
<button id='mic_btn' title='Microphone' disabled='true' class='material-icons'>mic</button>
<button id='location-modal-button' title='location console' class='material-icons'>location_on</button>
<button id='device-details-button' title='Device Details' class='material-icons'>info</button>
+ <button id='rotation-modal-button' title='Rotation sensors' class='material-icons'>more_vert</button>
</div>
<div id='control-panel-custom-buttons' class='control-panel-column'></div>
<!-- tabindex="-1" allows this element to capture keyboard events -->
@@ -199,11 +200,51 @@
</div>
</div>
-
+ <div id='rotation-modal' class='modal'>
+ <div id='rotation-modal-header' class='modal-header'>
+ <h2>Rotation sensors</h2>
+ <button id='rotation-modal-close' title='Close' class='material-icons modal-close'>close</button>
+ </div>
+ <hr>
+ <h3>Rotate the device</h3>
+ <span id='rotation-bar'>
+ <div class='roation-slider'>
+ X
+ <input class='rotation-slider-range' type='range' value='0' min='-180' max='180' step='0.1'>
+ <span class='rotation-slider-value'>0</span>
+ </div>
+ <br>
+ <div class='rotation-slider'>
+ Y
+ <input class='rotation-slider-range' type='range' value='0' min='-180' max='180' step='0.1'>
+ <span class='rotation-slider-value'>0</span>
+ </div>
+ <br>
+ <div class='rotation-slider'>
+ Z
+ <input class='rotation-slider-range' type='range' value='0' min='-180' max='180' step='0.1'>
+ <span class='rotation-slider-value'>0</span>
+ </div>
+ <br>
+ </span>
+ <div class='sensors'>
+ <div id='accelerometer'>
+ Accelerometer:
+ <span id='accelerometer-value'>0.00 9.81 0.00</span>
+ </div>
+ <div id='magnetometer'>Magnetometer:
+ <span id='magnetometer-value'>0 5.9 -48.4</span>
+ </div>
+ <div id='gyroscope'>Gyroscope:
+ <span id='gyroscope-value'>0.00 0.00 0.00</span>
+ </div>
+ </div>
+ </div>
<script src="js/adb.js"></script>
<script src="js/location.js"></script>
<script src="js/rootcanal.js"></script>
<script src="js/cf_webrtc.js" type="module"></script>
+ <script src="js/sensors.js"></script>
<script src="js/controls.js"></script>
<script src="js/app.js"></script>
<template id="display-template">
diff --git a/host/frontend/webrtc/html_client/js/app.js b/host/frontend/webrtc/html_client/js/app.js
index 03f2d13..f1e8360 100644
--- a/host/frontend/webrtc/html_client/js/app.js
+++ b/host/frontend/webrtc/html_client/js/app.js
@@ -143,6 +143,10 @@
#deviceCount = 0;
#micActive = false;
#adbConnected = false;
+ #motion = {
+ orientation: [0, 0, 0],
+ time: window.performance.now(),
+ };
constructor(deviceConnection, parentController) {
this.#deviceConnection = deviceConnection;
@@ -211,6 +215,9 @@
'device-details-button', 'device-details-modal',
'device-details-close');
createModalButton(
+ 'rotation-modal-button', 'rotation-modal',
+ 'rotation-modal-close');
+ createModalButton(
'bluetooth-modal-button', 'bluetooth-prompt', 'bluetooth-prompt-close');
createModalButton(
'bluetooth-prompt-wizard', 'bluetooth-wizard', 'bluetooth-wizard-close',
@@ -243,7 +250,7 @@
createModalButton(
'location-set-cancel', 'location-prompt-modal', 'location-set-modal-close',
'location-set-modal');
-
+ positionModal('rotation-modal-button', 'rotation-modal');
positionModal('device-details-button', 'bluetooth-modal');
positionModal('device-details-button', 'bluetooth-prompt');
positionModal('device-details-button', 'bluetooth-wizard');
@@ -273,6 +280,8 @@
createButtonListener('location-set-confirm', null, this.#deviceConnection,
evt => this.#onSendLocation(this.#deviceConnection, evt));
+ createSliderListener('rotation-slider', () => this.#onMotionChanged());
+
if (this.#deviceConnection.description.custom_control_panel_buttons.length >
0) {
document.getElementById('control-panel-custom-buttons').style.display =
@@ -421,6 +430,35 @@
let location_msg = longitude + "," +latitude + "," + altitude;
deviceConnection.sendLocationMessage(location_msg);
}
+
+ // Inject sensors' events on each change.
+ #onMotionChanged() {
+ const acc_val = document.getElementById('accelerometer-value');
+ const mgn_val = document.getElementById('magnetometer-value');
+ const gyro_val = document.getElementById('gyroscope-value');
+ let values = document.getElementsByClassName('rotation-slider-value');
+ let current_time = window.performance.now();
+ let xyz = [];
+ for (var i = 0; i < values.length; i++) {
+ xyz[i] = values[i].innerHTML;
+ }
+
+ // Calculate sensor values.
+ let acc_xyz = calculateAcceleration(xyz);
+ let mgn_xyz = calculateMagnetometer(xyz);
+ let time_dif = (current_time - this.#motion.time) * 1e-3;
+ let gyro_xyz = calculateGyroscope(this.#motion.orientation, xyz, time_dif);
+ this.#motion.time = current_time;
+ this.#motion.orientation = xyz;
+ // Inject sensors with new values.
+ adbShell(`/vendor/bin/cuttlefish_sensor_injection motion ${acc_xyz[0]} ${acc_xyz[1]} ${acc_xyz[2]} ${mgn_xyz[0]} ${mgn_xyz[1]} ${mgn_xyz[2]} ${gyro_xyz[0]} ${gyro_xyz[1]} ${gyro_xyz[2]}`);
+
+ // Display new sensor values after injection.
+ acc_val.textContent = `${acc_xyz[0]} ${acc_xyz[1]} ${acc_xyz[2]}`;
+ mgn_val.textContent = `${mgn_xyz[0]} ${mgn_xyz[1]} ${mgn_xyz[2]}`;
+ gyro_val.textContent = `${gyro_xyz[0]} ${gyro_xyz[1]} ${gyro_xyz[2]}`;
+ }
+
#onImportLocationsFile(deviceConnection, evt) {
function onLoad_send_kml_data(xml) {
diff --git a/host/frontend/webrtc/html_client/js/controls.js b/host/frontend/webrtc/html_client/js/controls.js
index eb21a34..09d7ad5 100644
--- a/host/frontend/webrtc/html_client/js/controls.js
+++ b/host/frontend/webrtc/html_client/js/controls.js
@@ -71,6 +71,28 @@
}
}
+// Bind the update of slider value to slider input,
+// and trigger a function to be called on input change and slider stop.
+function createSliderListener(slider_class, listener) {
+ const sliders = document.getElementsByClassName(slider_class + '-range');
+ const values = document.getElementsByClassName(slider_class + '-value');
+
+ for (let i = 0; i < sliders.length; i++) {
+ let slider = sliders[i];
+ let value = values[i];
+ // Trigger value update when the slider value changes while sliding.
+ slider.addEventListener('input', () => {
+ value.textContent = slider.value;
+ listener();
+ });
+ // Trigger value update when the slider stops sliding.
+ slider.addEventListener('change', () => {
+ listener();
+ });
+
+ }
+}
+
function createInputListener(input_id, func, listener) {
input = document.getElementById(input_id);
if (func != null) {
diff --git a/host/frontend/webrtc/html_client/js/sensors.js b/host/frontend/webrtc/html_client/js/sensors.js
new file mode 100644
index 0000000..4672e07
--- /dev/null
+++ b/host/frontend/webrtc/html_client/js/sensors.js
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const g = 9.80665; // meter per second^2
+const gravityVec = [0, g, 0];
+const magnetic_field = [0, 5.9, -48.4];
+
+function toRadians(x) {
+ return x * Math.PI / 180;
+}
+
+function determinantOfMatrix(M) {
+ // Only compute results for square matrices.
+ if (M.length != M[0].length) {
+ return 0;
+ }
+ if (M.length == 2) {
+ return M[0][0] * M[1][1] - M[1][0] * M[0][1];
+ }
+ let result = 0.0;
+ for (let i = 0; i < M.length; i++) {
+ let factor = M[0][i] * (i % 2? -1 : 1);
+ let subM = getSubmatrix(M, 0, i);
+ result += factor * determinantOfMatrix(subM);
+ }
+ return result;
+}
+
+// Get submatrix that excludes row i, column j.
+function getSubmatrix(M, i, j) {
+ let subM = [];
+ for (let k = 0; k < M.length; k++) {
+ if (k == i) {
+ continue;
+ }
+ let tmp = [];
+ for (let l = 0; l < M.length; l++) {
+ if (l == j) {
+ continue;
+ }
+ tmp.push(M[k][l]);
+ }
+ subM.push(tmp);
+ }
+ return subM;
+}
+
+function invertMatrix(M) {
+ // M ^ -1 = adj(M) / det(M)
+ // adj(M) = transpose(cofactor(M))
+ // Cij = (-1) ^ (i+j) det (Mij)
+ let det = determinantOfMatrix(M);
+ // If matrix is not invertible, return an empty matrix.
+ if (det == 0) {
+ return [[]];
+ }
+ let invM = [];
+ for (let i = 0; i < M.length; i++) {
+ let tmp = [];
+ for (let j = 0; j < M.length; j++) {
+ tmp.push(determinantOfMatrix(getSubmatrix(M, i,j)) * Math.pow(-1, i + j) / det);
+ }
+ invM.push(tmp);
+ }
+ invM = transposeMatrix(invM);
+ return invM;
+}
+
+function transposeMatrix(M) {
+ let transposedM = [];
+ for (let j = 0; j < M.at(0).length; j++) {
+ let tmp = [];
+ for (let i = 0; i < M.length; i++) {
+ tmp.push(M[i][j]);
+ }
+ transposedM.push(tmp);
+ }
+ return transposedM;
+}
+
+function matrixDotProduct(MA, MB) {
+ // If given matrices are not valid for multiplication,
+ // return an empty matrix.
+ if (MA[0].length != MB.length) {
+ return [[]];
+ }
+
+ let vec = [];
+ for (let r = 0; r < MA.length; r++) {
+ let tmp = [];
+ for (let c = 0; c < MB[0].length; c++) {
+ let dot = 0.0;
+ for (let i = 0; i < MA[0].length; i++) {
+ dot += MA[r][i] * MB[i][c];
+ }
+ tmp.push(dot);
+ }
+ vec.push(tmp);
+ }
+ return vec;
+}
+
+// Calculate the rotation matrix of the pitch, yaw, and roll angles.
+function getRotationMatrix(xR, yR, zR) {
+ xR = toRadians(-xR);
+ yR = toRadians(-yR);
+ zR = toRadians(-zR);
+ let rz = [[Math.cos(zR), -Math.sin(zR), 0],
+ [Math.sin(zR), Math.cos(zR), 0],
+ [0, 0, 1]];
+ let ry = [[Math.cos(yR), 0, Math.sin(yR)],
+ [0, 1, 0],
+ [-Math.sin(yR), 0, Math.cos(yR)]];
+ let rx = [[1, 0, 0],
+ [0, Math.cos(xR), -Math.sin(xR)],
+ [0, Math.sin(xR), Math.cos(xR)]];
+ let vec = matrixDotProduct(ry, rx);
+ vec = matrixDotProduct(rz, vec);
+ return vec;
+}
+
+// Calculate new Accelerometer values of the new rotation degrees.
+function calculateAcceleration(rotation) {
+ let rotationM = getRotationMatrix(rotation[0], rotation[1], rotation[2]);
+ let acc = transposeMatrix(matrixDotProduct(rotationM, transposeMatrix([gravityVec])))[0];
+ return acc.map((x) => x.toFixed(3));
+}
+
+// Calculate new Magnetometer values of the new rotation degrees.
+function calculateMagnetometer(rotation) {
+ let rotationM = getRotationMatrix(rotation[0], rotation[1], rotation[2]);
+ let mgn = transposeMatrix(matrixDotProduct(rotationM, transposeMatrix([magnetic_field])))[0];
+ return mgn.map((x) => x.toFixed(3));
+}
+
+// Convert rotation matrix to angular velocity numerator.
+function getAngularRotation(m) {
+ let trace = 0;
+ for (let i = 0; i < m.length; i++) {
+ trace += m[i][i];
+ }
+ let angle = Math.acos((trace - 1) / 2.0);
+ if (angle == 0) {
+ return [0, 0, 0];
+ }
+ let factor = 1.0 / (2 * Math.sin(angle));
+ let axis = [m[2][1] - m[1][2],
+ m[0][2] - m[2][0],
+ m[1][0] - m[0][1]];
+ // Get angular velocity numerator
+ return axis.map((x) => x * factor * angle);
+}
+
+// Calculate new Gyroscope values relative to the new rotation degrees.
+function calculateGyroscope(rotationA, rotationB, time_dif) {
+ let priorRotationM = getRotationMatrix(rotationA[0], rotationA[1], rotationA[2]);
+ let currentRotationM = getRotationMatrix(rotationB[0], rotationB[1], rotationB[2]);
+ let transitionMatrix = matrixDotProduct(priorRotationM, invertMatrix(currentRotationM));
+
+ const gyro = getAngularRotation(transitionMatrix);
+ return gyro.map((x) => (x / time_dif).toFixed(3));
+}
diff --git a/host/frontend/webrtc/html_client/style.css b/host/frontend/webrtc/html_client/style.css
index a9ee60a..0e00271 100644
--- a/host/frontend/webrtc/html_client/style.css
+++ b/host/frontend/webrtc/html_client/style.css
@@ -267,7 +267,12 @@
.location-button {
text-align: center;
}
-
+.sensors{
+ position: sticky;
+ right: 0;
+ top: 0;
+ text-align: right;
+}
.control-panel-column {
width: 50px;
/* Items inside this use a column Flexbox.*/