Android running in a virtualized environment normally has no access to the hardware of the host machine, most of the hardware it “works” with is instead emulated.
But sometimes one wants a way to attach external hardware to their custom Android without working with an actual physical Android device with a USB port. In our case we needed to attach a serial device.
In this article we will demonstrate how one can expose a serial port to Trout.
crosvm
Trout is based on crosvm, and crosvm already supports serial devices exposure using the --serial
parameter of the crosvm run
command.
But we use crosvm indirectly, through Trout, when we start it using the launch_cvd
command.
And launch_cvd
does not provide a way to pass values to the --serial
param of crosvm run
.
So we will add it.
Modifying the source code of your AOSP fork
Disclaimer: the diffs are shown for Android 14, diffs for other versions of Android would be slightly different.
First, cd device/google/cuttlefish
.
Now, let’s add new Cuttlefish config parameter, in the header file:
diff --git a/host/libs/config/cuttlefish_config.h b/host/libs/config/cuttlefish_config.h
index 9a2af86cd..7c9bb5725 100644
--- a/host/libs/config/cuttlefish_config.h
+++ b/host/libs/config/cuttlefish_config.h
@@ -125,6 +125,9 @@ class CuttlefishConfig {
void set_gem5_debug_flags(const std::string& gem5_debug_flags);
std::string gem5_debug_flags() const;
+ void set_attach_serial_device(const std::string& attach_serial_device);
+ std::string attach_serial_device() const;
+
void set_enable_host_uwb(bool enable_host_uwb);
bool enable_host_uwb() const;
…and in the implementation file:
diff --git a/host/libs/config/cuttlefish_config.cpp b/host/libs/config/cuttlefish_config.cpp
index 5c9e8dc36..2bdf2d6e4 100644
--- a/host/libs/config/cuttlefish_config.cpp
+++ b/host/libs/config/cuttlefish_config.cpp
@@ -188,6 +188,15 @@ void CuttlefishConfig::set_gem5_debug_flags(const std::string& gem5_debug_flags)
(*dictionary_)[kGem5DebugFlags] = gem5_debug_flags;
}
+static constexpr char kAttachSerialDevice[] = "attach_serial_device";
+void CuttlefishConfig::set_attach_serial_device(
+ const std::string& attach_serial_device) {
+ (*dictionary_)[kAttachSerialDevice] = attach_serial_device;
+}
+std::string CuttlefishConfig::attach_serial_device() const {
+ return (*dictionary_)[kAttachSerialDevice].asString();
+}
+
static constexpr char kWebRTCCertsDir[] = "webrtc_certs_dir";
void CuttlefishConfig::set_webrtc_certs_dir(const std::string& certs_dir) {
(*dictionary_)[kWebRTCCertsDir] = certs_dir;
Now let’s add a new parameter to the launch_cvd
command that would use this config change:
diff --git a/host/commands/assemble_cvd/flags.cc b/host/commands/assemble_cvd/flags.cc
index c5ef0344a..e10615d72 100644
--- a/host/commands/assemble_cvd/flags.cc
+++ b/host/commands/assemble_cvd/flags.cc
@@ -202,6 +202,16 @@ DEFINE_string(
seccomp_policy_dir, CF_DEFAULTS_SECCOMP_POLICY_DIR,
"With sandbox'ed crosvm, overrieds the security comp policy directory");
+DEFINE_string(
+ attach_serial_device, "",
+ "Path to a serial device that should be attached to crosvm "
+ "(/dev/<something>). "
+ "QEMU is not supported.\n"
+ "To see the attached device name inside the guest machine, look for a "
+ "corresponding log "
+ "message with the 'SERIAL_PORT' prefix during the VM start.\n"
+ "The device name is stable, but it may change on AOSP updates.");
+
DEFINE_vec(start_webrtc, cuttlefish::BoolToString(CF_DEFAULTS_START_WEBRTC),
"Whether to start the webrtc process.");
@@ -818,6 +828,8 @@ Result<CuttlefishConfig> InitializeCuttlefishConfiguration(
tmp_config_obj.set_gem5_debug_flags(FLAGS_gem5_debug_flags);
+ tmp_config_obj.set_attach_serial_device(FLAGS_attach_serial_device);
+
// streaming, webrtc setup
tmp_config_obj.set_webrtc_certs_dir(FLAGS_webrtc_certs_dir);
tmp_config_obj.set_sig_server_secure(FLAGS_webrtc_sig_server_secure);
Cuttlefish has already quite a few defined serial ports, let’s increase their number by one:
diff --git a/host/libs/vm_manager/vm_manager.h b/host/libs/vm_manager/vm_manager.h
index 4b116bfd9..036a2d3eb 100644
--- a/host/libs/vm_manager/vm_manager.h
+++ b/host/libs/vm_manager/vm_manager.h
@@ -54,7 +54,8 @@ class VmManager {
// - /dev/hvc9 = uwb
// - /dev/hvc10 = oemlock
// - /dev/hvc11 = keymint
- static const int kDefaultNumHvcs = 12;
+ static const int kDefaultNumHvcs =
+ 13; // NOTE: on merge conflicts do +1 to the upstream's value
// This is the number of virtual disks (block devices) that should be
// configured by the VmManager. Related to the description above regarding
And finally let’s actually forward the new parameter to crosvm
:
diff --git a/host/libs/vm_manager/crosvm_manager.cpp b/host/libs/vm_manager/crosvm_manager.cpp
index 100b71e56..d81b5eb08 100644
--- a/host/libs/vm_manager/crosvm_manager.cpp
+++ b/host/libs/vm_manager/crosvm_manager.cpp
@@ -449,6 +449,18 @@ Result<std::vector<MonitorCommand>> CrosvmManager::StartCommands(
for (auto i = 0; i < VmManager::kMaxDisks - disk_num; i++) {
crosvm_cmd.AddHvcSink();
}
+
+ if (!config.attach_serial_device().empty()) {
+ crosvm_cmd.AddHvcReadWrite(config.attach_serial_device(),
+ config.attach_serial_device());
+ LOG(INFO) << "SERIAL_PORT: attaching serial port to crosvm: "
+ << config.attach_serial_device()
+ << ", most likely device name inside the guest machine: /dev/hvc"
+ << crosvm_cmd.HvcNum() - 1;
+ } else {
+ crosvm_cmd.AddHvcSink();
+ }
+
CF_EXPECT(crosvm_cmd.HvcNum() + disk_num ==
VmManager::kMaxDisks + VmManager::kDefaultNumHvcs,
"HVC count (" << crosvm_cmd.HvcNum() << ") + disk count ("
… but not to QEMU, which is mostly used as AVD for normal (phone) Android apps (it might actually work, but we haven’t test it since our focus is Trout / crossvm):
diff --git a/host/libs/vm_manager/qemu_manager.cpp b/host/libs/vm_manager/qemu_manager.cpp
index a2744a35d..a0ae1286b 100644
--- a/host/libs/vm_manager/qemu_manager.cpp
+++ b/host/libs/vm_manager/qemu_manager.cpp
@@ -556,6 +556,8 @@ Result<std::vector<MonitorCommand>> QemuManager::StartCommands(
add_hvc_sink();
}
+ add_hvc_sink(); // attach_serial_device, not supported on QEMU
+
CF_EXPECT(
hvc_num + disk_num == VmManager::kMaxDisks + VmManager::kDefaultNumHvcs,
"HVC count (" << hvc_num << ") + disk count (" << disk_num << ") "
And the last thing we need to do is setting up proper access rights:
diff --git a/shared/config/ueventd.rc b/shared/config/ueventd.rc
index c604e3d23..dd7da4de3 100644
--- a/shared/config/ueventd.rc
+++ b/shared/config/ueventd.rc
@@ -44,5 +44,13 @@
# keymint / Rust
/dev/hvc11 0666 system system
+# Serial Port integration for the -attach_serial_device
+# param of the launch_cvd command.
+# Please update this name "/dev/hvcN" to "/dev/hvc{N+M}"
+# when kDefaultNumHvcs in 'host/libs/vm_manager/vm_manager.h' is updated.
+# The 'M' value here would be the delta between the new and the old values
+# of kDefaultNumHvcs.
+/dev/hvc12 0666 system system
+
# Factory Reset Protection
/dev/block/by-name/frp 0660 system system
You can find the full diff as one file here.
Conclusion
And that was basically it - now you can use the exposed port (/dev/hvc12
in the case of the diff above) from inside of Android.
Don’t forget to use the new parameter when running launch_cvd
, e.g.:
launch_cvd ... -attach_serial_device /dev/ttyACM0
Replace /dev/ttyACM0
with the actual path of the serial device on your host machine.
You can do a quick test by cross-compiling and adb-pushing a small binary to Android, which would attach to the serial port, and then executing the binary under su
. Or just connect to the port from any system service.