Add rootcanal test and HCI channel communication facilities.
Rootcanal listens on TCP sockets for test commands and HCI commands.
The test channel is for adding/managing pre-made devices, and the HCI
channel is to control a custom H4 HCI device via direct HCI commands.
This exposes those to STS tests and allow easy communication via those.
Also automated port management so that there is no host-side port
collision when running a test on multiple devices at once.
Test: custom sts test that creates a device, adds it to the controller,
then list devices
Bug: 216394844
Change-Id: I8cec46c9efab7aca824a1b3a0343499fd23cadd1
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
index 3849b95..c76ed5e 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
@@ -16,14 +16,23 @@
package com.android.sts.common;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static com.android.sts.common.CommandUtil.runAndCheck;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
+import java.io.InputStream;
import java.io.IOException;
import java.io.StringReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
import java.util.List;
@@ -74,34 +83,31 @@
assertNotNull("Device not set", device);
try {
device.enableAdbRoot();
+ ProcessUtil.killAll(
+ device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000, false);
runAndCheck(device, String.format("rm -rf '%s'", LOCK_FILENAME));
device.disableAdbRoot();
// OverlayFsUtils' finished() will restart the device.
overlayFsUtils.finished(d);
+ runAndCheck(device, "svc bluetooth enable");
} catch (DeviceNotAvailableException e) {
throw new AssertionError("Device unavailable when cleaning up", e);
+ } catch (TimeoutException e) {
+ CLog.w("Could not kill rootcanal HAL during cleanup");
}
}
- /** Replace existing HAL with RootCanal HAL on current device. */
- public void enableRootcanal()
- throws DeviceNotAvailableException, IOException, InterruptedException,
- TimeoutException {
- enableRootcanal(6111);
- }
-
/**
* Replace existing HAL with RootCanal HAL on current device.
*
- * @param port host TCP port to adb-forward to rootcanal control port.
+ * @return an instance of RootcanalController
*/
- public void enableRootcanal(int port)
+ public RootcanalController enableRootcanal()
throws DeviceNotAvailableException, IOException, InterruptedException,
TimeoutException {
ITestDevice device = test.getDevice();
- CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild());
assertNotNull("Device not set", device);
- assertNotNull("Build not set", buildHelper);
+ CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild());
// Check and made sure we're not calling this more than once for a device
assertFalse("rootcanal set up called more than once", device.doesFileExist(LOCK_FILENAME));
@@ -202,7 +208,14 @@
}
}
- device.executeAdbCommand("forward", "tcp:6111", String.format("tcp:%d", port));
+ // Forward root canal control ports on the device to the host
+ int testPort = findOpenPort();
+ device.executeAdbCommand("forward", String.format("tcp:%d", testPort), "tcp:6111");
+
+ int hciPort = findOpenPort();
+ device.executeAdbCommand("forward", String.format("tcp:%d", hciPort), "tcp:6211");
+
+ return new RootcanalController(testPort, hciPort);
}
private void tryUpdateVintfManifest(ITestDevice device)
@@ -269,4 +282,182 @@
TimeUnit.MILLISECONDS.sleep(250);
}
}
+
+ /** Find an open TCP port on the host */
+ private static int findOpenPort() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ socket.setReuseAddress(true);
+ return socket.getLocalPort();
+ }
+ }
+
+ /** Class that encapsulates a virtual HCI device that can be controlled by HCI commands. */
+ public static class HciDevice implements AutoCloseable {
+ private static final String READ_FAIL_MSG = "Failed to read HCI packet";
+ private final Socket hciSocket;
+
+ private HciDevice(Socket hciSocket) {
+ this.hciSocket = hciSocket;
+ }
+
+ @Override
+ public void close() throws IOException {
+ hciSocket.close();
+ }
+
+ /**
+ * Convenient wrapper around sendHciPacket to send a HCI command packet to device.
+ *
+ * @param ogf Opcode group field
+ * @param ocf Opcode command field
+ * @param params the rest of the command parameters
+ */
+ public void sendHciCmd(int ogf, int ocf, byte[] params) throws IOException {
+ assertTrue("params length must be less than 256 bytes", params.length < 256);
+ ByteBuffer cmd = ByteBuffer.allocate(4 + params.length).order(ByteOrder.LITTLE_ENDIAN);
+ int opcode = (ogf << 10) | ocf;
+ cmd.put((byte) 0x01).putShort((short) opcode).put((byte) params.length).put(params);
+ sendHciPacket(cmd.array());
+ }
+
+ /**
+ * Send raw HCI packet to device.
+ *
+ * @param packet raw packet data to send to device
+ */
+ public void sendHciPacket(byte[] packet) throws IOException {
+ hciSocket.getOutputStream().write(packet);
+ }
+
+ /** Read one HCI packet from device, blocking until data is available. */
+ public byte[] readHciPacket() throws IOException {
+ ByteArrayOutputStream ret = new ByteArrayOutputStream();
+ InputStream in = hciSocket.getInputStream();
+
+ // Read the packet type
+ byte[] typeBuf = new byte[1];
+ assertEquals(READ_FAIL_MSG, 1, in.read(typeBuf, 0, 1));
+ ret.write(typeBuf);
+
+ // Read the header and figure out how much data to read
+ // according to BT core spec 5.2 vol 4 part A section 2 & part E section 5.4
+ byte[] hdrBuf = new byte[4];
+ int dataLength;
+
+ switch (typeBuf[0]) {
+ case 0x01: // Command packet
+ case 0x03: // Synch data packet
+ assertEquals(READ_FAIL_MSG, 3, in.read(hdrBuf, 0, 3));
+ ret.write(hdrBuf, 0, 3);
+ dataLength = hdrBuf[2];
+ break;
+
+ case 0x02: // Async data packet
+ assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4));
+ ret.write(hdrBuf, 0, 4);
+ dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFF) << 8);
+ break;
+
+ case 0x04: // Event
+ assertEquals(READ_FAIL_MSG, 2, in.read(hdrBuf, 0, 2));
+ ret.write(hdrBuf, 0, 2);
+ dataLength = hdrBuf[1];
+ break;
+
+ case 0x05: // ISO synch data packet
+ assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4));
+ ret.write(hdrBuf, 0, 4);
+ dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFC) << 6);
+ break;
+
+ default:
+ throw new IOException("Unexpected packet type: " + String.valueOf(typeBuf[0]));
+ }
+
+ // Read the data payload
+ byte[] data = new byte[dataLength];
+ assertEquals(READ_FAIL_MSG, dataLength, in.read(data, 0, dataLength));
+ ret.write(data, 0, dataLength);
+
+ return ret.toByteArray();
+ }
+ }
+
+ public static class RootcanalController implements AutoCloseable {
+ private final int testPort;
+ private final int hciPort;
+ private Socket rootcanalTestChannel = null;
+ private List<HciDevice> hciDevices = new ArrayList<>();
+
+ private RootcanalController(int testPort, int hciPort)
+ throws IOException, InterruptedException {
+ this.testPort = testPort;
+ this.hciPort = hciPort;
+ CLog.d("Rootcanal controller initialized; testPort=%d, hciPort=%d", testPort, hciPort);
+ }
+
+ @Override
+ public void close() throws IOException {
+ rootcanalTestChannel.close();
+ for (HciDevice dev : hciDevices) {
+ dev.close();
+ }
+ }
+
+ /**
+ * Create a new HCI device by connecting to rootcanal's HCI socket.
+ *
+ * @return HciDevice object that allows sending/receiving from the HCI port
+ */
+ public HciDevice createHciDevice()
+ throws DeviceNotAvailableException, IOException, InterruptedException {
+ HciDevice dev = new HciDevice(new Socket("localhost", hciPort));
+ hciDevices.add(dev);
+ return dev;
+ }
+
+ /**
+ * Send one command to rootcanal test channel.
+ *
+ * <p>Send `help` command for list of accepted commands from Rootcanal.
+ *
+ * @param cmd command to send
+ * @param args arguments for the command
+ * @return Response string from rootcanal
+ */
+ public String sendTestChannelCommand(String cmd, String... args)
+ throws IOException, InterruptedException {
+ if (rootcanalTestChannel == null) {
+ rootcanalTestChannel = new Socket("localhost", testPort);
+ CLog.d(
+ "RootCanal test channel init: "
+ + readTestChannel(rootcanalTestChannel.getInputStream()));
+ }
+
+ // Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py
+ ByteArrayOutputStream msg = new ByteArrayOutputStream();
+ msg.write(cmd.length());
+ msg.write(cmd.getBytes("ASCII"));
+ msg.write(args.length);
+ for (String arg : args) {
+ msg.write(arg.length());
+ msg.write(arg.getBytes("ASCII"));
+ }
+
+ rootcanalTestChannel.getOutputStream().write(msg.toByteArray());
+ return readTestChannel(rootcanalTestChannel.getInputStream());
+ }
+
+ /** Read one message from rootcanal test channel. */
+ private String readTestChannel(InputStream in) throws IOException {
+ // Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py
+ ByteBuffer sizeBuf = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN);
+ in.read(sizeBuf.array(), 0, Integer.BYTES);
+ int size = sizeBuf.getInt();
+
+ byte[] buf = new byte[size];
+ in.read(buf, 0, size);
+ return new String(buf);
+ }
+ }
}