typecd: Add Cable speed metric

Report the cables supported USB speed based on the value reported in
the cable VDO.

BUG=b:185157607
TEST=Unit tests still pass. typec.Mode* tests pass.

Change-Id: I8af59c551f0a4c55f20844b0613c93aa18fbe5a5
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2822891
Tested-by: Prashant Malani <pmalani@chromium.org>
Commit-Queue: Prashant Malani <pmalani@chromium.org>
Reviewed-by: Mengqi Guo <mqg@chromium.org>
diff --git a/typecd/cable.cc b/typecd/cable.cc
index 3a56cf7..0231dbb 100644
--- a/typecd/cable.cc
+++ b/typecd/cable.cc
@@ -156,10 +156,76 @@
   return num_alt_modes_ == alt_modes_.size();
 }
 
+CableSpeedMetric Cable::GetCableSpeedMetric() {
+  CableSpeedMetric ret = CableSpeedMetric::kOther;
+
+  // If we can't identify a valid cable in the ID Header, return early.
+  auto cable_type = (GetIdHeaderVDO() >> kIDHeaderVDOProductTypeBitOffset) &
+                    kIDHeaderVDOProductTypeMask;
+  if (!(cable_type == kIDHeaderVDOProductTypeCableActive ||
+        cable_type == kIDHeaderVDOProductTypeCablePassive))
+    return ret;
+
+  // Parse the speed field in the Cable VDO.
+  auto cable_vdo = GetProductTypeVDO1();
+  uint32_t speed = cable_vdo & kUSBSpeedBitMask;
+  switch (speed) {
+    case kUSBSpeed20:
+      ret = CableSpeedMetric::kUSB2_0;
+      break;
+    case kUSBSuperSpeed32Gen1:
+      ret = CableSpeedMetric::kUSB3_2Gen1;
+      break;
+    case kUSBSuperSpeed32Or40Gen2:
+      ret = CableSpeedMetric::kUSB3_2USB4Gen2;
+      break;
+    case kUSB40SuperSpeedGen3:
+      ret = CableSpeedMetric::kUSB4Gen3;
+      break;
+  }
+
+  // Add special handling for the PD 2.0 Cable VDO speed.
+  if (GetPDRevision() == PDRevision::k20) {
+    if (speed == kUSBSuperSpeed31Gen1) {
+      ret = CableSpeedMetric::kUSB3_1Gen1;
+    } else if (speed == kUSBSuperSpeed31Gen2) {
+      ret = CableSpeedMetric::kUSB3_1Gen1Gen2;
+    }
+  }
+
+  if (ret != CableSpeedMetric::kUSB2_0)
+    return ret;
+
+  // Finally, handle TBT-only cables (only if the VDOs claim to only
+  // support USB 2.0 speeds).
+  for (const auto& [index, mode] : alt_modes_) {
+    if (mode->GetSVID() != kTBTAltModeVID)
+      continue;
+
+    uint32_t tbt_vdo = mode->GetVDO();
+
+    // If rounded support is there, we should continue.
+    auto rounded_support =
+        (tbt_vdo >> kTBT3CableDiscModeVDORoundedSupportOffset) &
+        kTBT3CableDiscModeVDORoundedSupportMask;
+    if (rounded_support == kTBT3CableDiscModeVDO_3_4_Gen_Rounded_Non_Rounded)
+      continue;
+
+    auto speed = (tbt_vdo >> kTBT3CableDiscModeVDOSpeedOffset) &
+                 kTBT3CableDiscModeVDOSpeedMask;
+    if (speed == kTBT3CableDiscModeVDOSpeed10G20G)
+      ret = CableSpeedMetric::kTBTOnly10G20G;
+  }
+
+  return ret;
+}
+
 void Cable::ReportMetrics(Metrics* metrics) {
   if (!metrics || metrics_reported_)
     return;
 
+  metrics->ReportCableSpeed(GetCableSpeedMetric());
+
   metrics_reported_ = true;
 }
 
diff --git a/typecd/cable.h b/typecd/cable.h
index e584212..683dde4 100644
--- a/typecd/cable.h
+++ b/typecd/cable.h
@@ -85,6 +85,13 @@
   void ReportMetrics(Metrics* metrics);
 
  private:
+  friend class MetricsTest;
+  FRIEND_TEST(MetricsTest, CheckCableSpeedTBTOnly);
+  FRIEND_TEST(MetricsTest, CheckCableSpeedPassive40Gbps);
+  FRIEND_TEST(MetricsTest, CheckCableSpeedPassiveUSB31_Gen1);
+
+  CableSpeedMetric GetCableSpeedMetric();
+
   // Map representing all SOP' alternate modes.
   // The key is the index of the alternate mode as determined
   // by the connector class sysfs directory. For example,
diff --git a/typecd/metrics.cc b/typecd/metrics.cc
index edd83b4..b06ad3f 100644
--- a/typecd/metrics.cc
+++ b/typecd/metrics.cc
@@ -6,6 +6,7 @@
 
 namespace {
 constexpr char kPartnerTypeMetricName[] = "ChromeOS.TypeC.PartnerType";
+constexpr char kCableSpeedMetricName[] = "ChromeOS.TypeC.CableSpeed";
 }  // namespace
 
 namespace typecd {
@@ -19,4 +20,13 @@
   }
 }
 
+void Metrics::ReportCableSpeed(CableSpeedMetric speed) {
+  if (!metrics_library_.SendEnumToUMA(
+          kCableSpeedMetricName, static_cast<int>(speed),
+          static_cast<int>(CableSpeedMetric::kMaxValue) + 1)) {
+    LOG(WARNING) << "Failed to send cable speed sample to UMA, speed: "
+                 << static_cast<int>(speed);
+  }
+}
+
 }  // namespace typecd
diff --git a/typecd/metrics.h b/typecd/metrics.h
index f3c97bf..031eac8 100644
--- a/typecd/metrics.h
+++ b/typecd/metrics.h
@@ -26,6 +26,20 @@
   kMaxValue = kUSBPeripheral,
 };
 
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class CableSpeedMetric {
+  kOther = 0,
+  kUSB2_0 = 1,
+  kUSB3_2Gen1 = 2,
+  kUSB3_2USB4Gen2 = 3,
+  kUSB3_1Gen1 = 4,
+  kUSB3_1Gen1Gen2 = 5,
+  kUSB4Gen3 = 6,
+  kTBTOnly10G20G = 7,
+  kMaxValue = kTBTOnly10G20G,
+};
+
 // A class for collecting UMA metrics.
 class Metrics {
  public:
@@ -36,6 +50,7 @@
   ~Metrics() = default;
 
   void ReportPartnerType(PartnerTypeMetric type);
+  void ReportCableSpeed(CableSpeedMetric speed);
 
  private:
   MetricsLibrary metrics_library_;
diff --git a/typecd/metrics_test.cc b/typecd/metrics_test.cc
index d97de9a..87323cf 100644
--- a/typecd/metrics_test.cc
+++ b/typecd/metrics_test.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "typecd/cable.h"
 #include "typecd/partner.h"
 
 #include <base/files/scoped_temp_dir.h>
@@ -182,4 +183,65 @@
   EXPECT_EQ(PartnerTypeMetric::kOther, p.GetPartnerTypeMetric());
 }
 
+TEST_F(MetricsTest, CheckCableSpeedTBTOnly) {
+  // Belkin TBT3 Active Cable 40Gbps.
+  Cable c(base::FilePath("foo"));
+
+  c.SetPDRevision(PDRevision::k20);
+  c.SetIdHeaderVDO(0x240020c2);
+  c.SetCertStatVDO(0x0);
+  c.SetProductVDO(0x40010);
+  c.SetProductTypeVDO1(0x21085858);
+  c.SetProductTypeVDO2(0x0);
+  c.SetProductTypeVDO3(0x0);
+
+  c.SetNumAltModes(2);
+
+  std::string mode_dirname = base::StringPrintf("port%d-plug0.%d", 0, 0);
+  auto mode_path = temp_dir_.Append(mode_dirname);
+  ASSERT_TRUE(CreateFakeAltMode(mode_path, kTBTAltModeVID, 0x430001, 0));
+  c.AddAltMode(mode_path);
+
+  mode_dirname = base::StringPrintf("port%d-plug0.%d", 0, 1);
+  mode_path = temp_dir_.Append(mode_dirname);
+  ASSERT_TRUE(CreateFakeAltMode(mode_path, 0x04b4, 0x1, 0));
+  c.AddAltMode(mode_path);
+
+  EXPECT_EQ(CableSpeedMetric::kTBTOnly10G20G, c.GetCableSpeedMetric());
+}
+
+TEST_F(MetricsTest, CheckCableSpeedPassive40Gbps) {
+  // StarTech Passive Cable 40 Gbps PD 2.0
+  Cable c(base::FilePath("foo"));
+
+  c.SetPDRevision(PDRevision::k20);
+  c.SetIdHeaderVDO(0x1c0020c2);
+  c.SetCertStatVDO(0x000000b6);
+  c.SetProductVDO(0x00010310);
+  c.SetProductTypeVDO1(0x11082052);
+  c.SetProductTypeVDO2(0x0);
+  c.SetProductTypeVDO3(0x0);
+
+  c.SetNumAltModes(0);
+
+  EXPECT_EQ(CableSpeedMetric::kUSB3_1Gen1Gen2, c.GetCableSpeedMetric());
+}
+
+TEST_F(MetricsTest, CheckCableSpeedPassiveUSB31_Gen1) {
+  // Hongju Full USB 3.1 Gen 1 5A passive cable.
+  Cable c(base::FilePath("foo"));
+
+  c.SetPDRevision(PDRevision::k20);
+  c.SetIdHeaderVDO(0x18005694);
+  c.SetCertStatVDO(0x88);
+  c.SetProductVDO(0xce901a0);
+  c.SetProductTypeVDO1(0x84051);
+  c.SetProductTypeVDO2(0x0);
+  c.SetProductTypeVDO3(0x0);
+
+  c.SetNumAltModes(0);
+
+  EXPECT_EQ(CableSpeedMetric::kUSB3_1Gen1, c.GetCableSpeedMetric());
+}
+
 }  // namespace typecd
diff --git a/typecd/pd_vdo_constants.h b/typecd/pd_vdo_constants.h
index f1c19b9..0ea9814 100644
--- a/typecd/pd_vdo_constants.h
+++ b/typecd/pd_vdo_constants.h
@@ -62,6 +62,9 @@
 constexpr uint8_t kTBT3CableDiscModeVDORoundedSupportOffset = 19;
 constexpr uint8_t kTBT3CableDiscModeVDORoundedSupportMask = 0x3;
 constexpr uint8_t kTBT3CableDiscModeVDO_3_4_Gen_Rounded_Non_Rounded = 0x1;
+constexpr uint8_t kTBT3CableDiscModeVDOSpeedOffset = 16;
+constexpr uint8_t kTBT3CableDiscModeVDOSpeedMask = 0x7;
+constexpr uint8_t kTBT3CableDiscModeVDOSpeed10G20G = 0x3;
 
 // Standard and Vendor Indentifications commonly expected in cables and partners
 constexpr uint16_t kDPAltModeSID = 0xff01;