huddly-updater: enable dual stream by default

Huddly GO camera can be configured to single, dual and triple stream
modes. For use in CfM, we need it to always be in dual stream mode. For
some time, we have been using non-standard firmware to ensure the camera
ran in dual stream mode. With this patch, we can go back to using
standard firmware and do the stream configuration once the camera
connects to CfM.

BUG=b:63618651
TEST=Connect and disconnect camera with different initial streaming
modes to CfM. Inspect logs to see if huddly-updater configured the
stream mode to the desired value.

Change-Id: I502ec4689979b3637951d63abc6498020d638e25
Reviewed-on: https://chromium-review.googlesource.com/808994
Commit-Ready: Ege Mihmanli <egemih@google.com>
Tested-by: Ege Mihmanli <egemih@google.com>
Reviewed-by: Zhongze Hu <frankhu@google.com>
diff --git a/src/main.cc b/src/main.cc
index b236b16..4e2e6bc 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -42,6 +42,10 @@
 // the Huddly peripheral in use.
 void ShowInfo(const CmdlineFlags& cmd_flags);
 
+// Configures the number of streams sent
+// from Huddly camera to the desired value.
+bool ConfigStreamMode();
+
 // Show command line argument options.
 void ShowUsage(const char* program_name);
 
@@ -126,7 +130,12 @@
   if (do_prep) {
     LOG(INFO) << flasher.GetUpgradeEligibilityMessage();
     if (!flasher.IsEligibleForUpgrade()) {
-      LOG(INFO) << ".. No need to update. Exiting huddly-updater.";
+
+      // If configuring stream mode fails, or is not necessary, display message
+      // and quit.
+      if(!ConfigStreamMode())
+        LOG(INFO) << ".. Exiting huddly-updater.";
+
       return EXIT_SUCCESS;
     }
 
@@ -164,6 +173,7 @@
 
     LOG(INFO) << ".. upgrade complete.";
   }
+
   return EXIT_SUCCESS;
 }
 
@@ -215,22 +225,56 @@
   minidev.ShowInfo();
 }
 
+bool ConfigStreamMode() {
+  const huddly::StreamMode kMode = huddly::StreamMode::DUAL;
+  huddly::MinicamDevice minidev(huddly::kVendorId, huddly::kProductIdApp);
+  std::string err_msg;
+  if (!minidev.Setup(&err_msg)) {
+    LOG(ERROR) << err_msg;
+    return false;
+  }
+
+  huddly::StreamMode current_mode;
+  if (!minidev.GetStreamMode(&current_mode)) {
+    minidev.Teardown(&err_msg);
+    LOG(ERROR) << "Failed to get stream mode.";
+    return false;
+  }
+
+  if (current_mode == kMode) {
+    minidev.Teardown(&err_msg);
+    LOG(INFO) << ".. Already in " << huddly::StreamModeToStr(kMode) << " mode.";
+    return false;
+  }
+
+  if (!minidev.SetStreamMode(kMode, &err_msg)) {
+    minidev.Teardown(&err_msg);
+    LOG(ERROR) << "failed to set stream mode " << err_msg;
+    return false;
+  }
+
+  LOG(INFO) << "Changed stream mode from "
+            << huddly::StreamModeToStr(current_mode) << " to "
+            << huddly::StreamModeToStr(kMode) << ". Rebooting.";
+  minidev.Reboot(&err_msg);
+  return true;
+}
+
 void ShowUsage(const char* program_name) {
-  // TODO(porce): Refactor using const string.
   LOG(INFO) << "Show usage";
   /* clang-format off */
   printf("\n"
     "Usage: %s [--option(s)]\n"
-    "  --help:    Show this usage.\n"
-    "  --info:    Show firmware and peripheral information.\n"
-    "  --force:   Force the upgrade ignoring the upgrade eligibility check.\n"
-    "  --dryrun:  Dryrun. Do everything except for final committment.\n"
-    "  --pkg:     Specify the absolute path for firmware package file.\n"
-    "  --log_to:  Path to log file. stdout is legal. \n"
-    "  --prep:    Prep only, without entering the burning step.\n"
-    "  --burn:    Burn only, without prepping step.\n"
     "  --app:     Switch to App mode and reboot.\n"
     "  --boot:    Switch to Boot mode and reboot.\n"
+    "  --burn:    Burn only, without prepping step.\n"
+    "  --dryrun:  Dryrun. Do everything except for final committment.\n"
+    "  --force:   Force the upgrade ignoring the upgrade eligibility check.\n"
+    "  --help:    Show this message and quit.\n"
+    "  --info:    Show firmware and peripheral information.\n"
+    "  --log_to:  Path to log file. stdout is legal.\n"
+    "  --pkg:     Specify the absolute path for firmware package file.\n"
+    "  --prep:    Prep only, without entering the burning step.\n"
     "\n",
     program_name);
   /* clang-format on */
@@ -240,16 +284,16 @@
 void ParseArgs(int argc, char* argv[], CmdlineFlags* flags) {
   const char* kOptString = "h";
   const struct option long_options[] = {
-      {"help", no_argument, 0, 'h'},
-      {"info", no_argument, 0, 'i'},
-      {"force", no_argument, 0, 'f'},
-      {"dryrun", no_argument, 0, 'd'},
-      {"pkg", required_argument, 0, 'p'},
+      {"app",    no_argument,       0, 'a'},
+      {"boot",   no_argument,       0, 'b'},
+      {"burn",   no_argument,       0, 'u'},
+      {"dryrun", no_argument,       0, 'd'},
+      {"force",  no_argument,       0, 'f'},
+      {"help",   no_argument,       0, 'h'},
+      {"info",   no_argument,       0, 'i'},
       {"log_to", required_argument, 0, 'l'},
-      {"prep", no_argument, 0, 'r'},
-      {"burn", no_argument, 0, 'u'},
-      {"app", no_argument, 0, 'a'},
-      {"boot", no_argument, 0, 'b'},
+      {"pkg",    required_argument, 0, 'p'},
+      {"prep",   no_argument,       0, 'r'},
       {0, 0, 0, 0},
   };
 
diff --git a/src/minicam_device.cc b/src/minicam_device.cc
index fc2cac2..fe9d3ed 100644
--- a/src/minicam_device.cc
+++ b/src/minicam_device.cc
@@ -29,8 +29,8 @@
 const uint64_t kTypicalCommitTimeMs = 30000;
 
 // Handling firmware response from huddly.
-const uint8_t kAppVersionOffset= 1;
-const uint8_t kBootloaderVersionOffset= 5;
+const uint8_t kAppVersionOffset = 1;
+const uint8_t kBootloaderVersionOffset = 5;
 
 uint16_t BootModeToProductId(BootMode mode) {
   switch (mode) {
@@ -45,6 +45,25 @@
   }
 }
 
+std::string StreamModeToStr(StreamMode stream_mode) {
+  switch (stream_mode) {
+    case StreamMode::SINGLE:
+      return "Single";
+    case StreamMode::DUAL:
+      return "Dual";
+    case StreamMode::TRIPLE:
+      return "Triple";
+    case StreamMode::WRITE_ENABLE:
+      return "Write Enable";
+    default:
+      uint16_t unknown_val = static_cast<uint16_t>(stream_mode);
+      char return_string[15];
+      snprintf(return_string, sizeof(return_string), "UNKNOWN:0x%04x",
+               unknown_val);
+      return return_string;
+  }
+}
+
 MinicamDevice::MinicamDevice(uint16_t vendor_id, uint16_t product_id)
     : UsbDevice(vendor_id, product_id) {}
 
@@ -273,6 +292,65 @@
   return false;
 }
 
+bool MinicamDevice::GetStreamMode(StreamMode* stream_mode) const {
+  const uint8_t kRequestType = 0xa1;  // UVC command.
+  const uint8_t kRequest = 0x81;      // GET_CUR.
+  const uint16_t kValue = 0x0100;     // XU_CONTROL value 0x0001
+  const uint16_t kIndex = 0x0400;     // Unit number 4, interface 0
+  const uint32_t kDataLen = 2;
+  uint8_t data[kDataLen];
+  uint16_t mode_val;
+  std::string err_msg;
+
+  bool ret = ControlTransfer(kRequestType, kRequest, kValue, kIndex, kDataLen,
+                             data, &err_msg);
+
+  if (!ret) {
+    LOG(ERROR) << err_msg;
+    return false;
+  }
+
+  // First treat as little endian, convert to host endian after.
+  mode_val = (data[1] << 8) | data[0];
+  *stream_mode = static_cast<StreamMode>(le16toh(mode_val));
+
+  return true;
+}
+
+bool MinicamDevice::SetStreamMode(StreamMode stream_mode,
+                                  std::string* err_msg) const {
+  const uint8_t kRequestType = 0x21;
+  const uint8_t kRequest = 0x01;
+  const uint16_t kValue = 0x0100;
+  const uint16_t kIndex = 0x0400;
+  const uint32_t kDataLen = 2;
+  uint8_t data[kDataLen];
+  uint16_t mode_val = htole16(static_cast<uint16_t>(StreamMode::WRITE_ENABLE));
+
+  data[0] = mode_val;
+  data[1] = mode_val >> 8;
+  bool ret = ControlTransfer(kRequestType, kRequest, kValue, kIndex, kDataLen,
+                             data, err_msg);
+
+  if (!ret) {
+    *err_msg += " ... failed to write enable camera";
+    return false;
+  }
+
+  mode_val = htole16(static_cast<uint16_t>(stream_mode));
+  data[0] = mode_val;
+  data[1] = mode_val >> 8;
+  ret = ControlTransfer(kRequestType, kRequest, kValue, kIndex, kDataLen, data,
+                        err_msg);
+
+  if (!ret) {
+    *err_msg += "... failed to set stream configuration";
+    return false;
+  }
+
+  return true;
+}
+
 bool MinicamDevice::GetBootMode(BootMode* boot_mode,
                                 std::string* err_msg) const {
   uint8_t data;
@@ -560,14 +638,8 @@
   return Mv2Read(sizeof(data), &data, &err_msg);
 }
 
-// Assumes firmware version has 3 separate bytes
-// stored in reverse order.
-std::string MinicamDevice::FormatVersion(uint8_t* data) const{
-  if(sizeof(data) < 3){
-    return "unknown";
-  }
-  return std::to_string(data[2]) + "." +
-         std::to_string(data[1]) + "." +
+std::string MinicamDevice::FormatVersion(uint8_t* data) const {
+  return std::to_string(data[2]) + "." + std::to_string(data[1]) + "." +
          std::to_string(data[0]);
 }
 
diff --git a/src/minicam_device.h b/src/minicam_device.h
index fa09210..27bbef5 100644
--- a/src/minicam_device.h
+++ b/src/minicam_device.h
@@ -40,9 +40,18 @@
   UNKNOWN = 0x10,
 };
 
+enum class StreamMode : uint16_t {
+  SINGLE = 0x0000,
+  DUAL = 0x0001,
+  TRIPLE = 0x0002,
+  WRITE_ENABLE = 0x8eb0,
+};
+
 uint16_t BootModeToProductId(BootMode mode);
 std::string BootModeStr(BootMode boot_mode);
 
+std::string StreamModeToStr(StreamMode stream_mode);
+
 class MinicamDevice : public UsbDevice {
  public:
   MinicamDevice(uint16_t vendor_id, uint16_t product_id);
@@ -69,6 +78,9 @@
   // Works in App mode only.
   bool GetVersion(std::string* app_ver, std::string* bootloader_ver);
 
+  bool GetStreamMode(StreamMode* stream_mode) const;
+  bool SetStreamMode(StreamMode stream_mode, std::string* err_msg) const;
+
  private:
   MinicamDevice(const MinicamDevice&) = delete;
   MinicamDevice& operator=(const MinicamDevice&) = delete;
diff --git a/src/usb_device.cc b/src/usb_device.cc
index 84ea722..a375790 100644
--- a/src/usb_device.cc
+++ b/src/usb_device.cc
@@ -116,14 +116,6 @@
     return false;
   }
 
-  /*  TODO(porce): Investigate the need of configuring Huddly camera
-    int configuration = 1;  // bConfigurationValue
-    if ((ret = libusb_set_configuration(dev_handle_, configuration)) != 0) {
-      *err_msg = ErrCodeToErrMsg(ret);
-      return false;
-    }
-  */
-
   if (!ClaimInterface(err_msg)) {
     *err_msg += ".. failed to setup " + GetId();
     Teardown(err_msg);  // Escape sequence. No return check.