Bidirectional test mode signaling over GPIO

* Implements a bidirectional synchronous handshake protocol over
  dut_flaga/b GPIOs. This protocol is designed to return true (test
  mode) only if the DUT is connected to a servo board which implements
  the remote end.

* Includes unit tests for the test mode signaling routine, complete with
  mock/fake implementation of the remote end.

Note that we still do not deploy GpioHandler in actual update
processing, which will be done later.

BUG=chromium-os:25397,chromium-os:27109,chromium-os:27672
TEST=Builds and passes unit tests (including new ones)

Change-Id: I265407ed735c3e1354e10782ac30566b16caeb20
Reviewed-on: https://gerrit.chromium.org/gerrit/23330
Reviewed-by: Gaurav Shah <gauravsh@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Reviewed-by: Gilad Arnold <garnold@chromium.org>
diff --git a/gpio_handler.cc b/gpio_handler.cc
index efd5e3d..7145bbb 100644
--- a/gpio_handler.cc
+++ b/gpio_handler.cc
@@ -30,23 +30,30 @@
 
 const StandardGpioHandler::GpioDef
 StandardGpioHandler::gpio_defs_[kGpioIdMax] = {
-  { "dutflaga", "ID_GPIO_DUTFLAGA" },  // kGpioDutflaga
-  { "dutflagb", "ID_GPIO_DUTFLAGB" },  // kGpioDutflagb
+  { "dutflaga", "ID_GPIO_DUTFLAGA" },  // kGpioIdDutflaga
+  { "dutflagb", "ID_GPIO_DUTFLAGB" },  // kGpioIdDutflagb
 };
 
 unsigned StandardGpioHandler::num_instances_ = 0;
 
 
 StandardGpioHandler::StandardGpioHandler(UdevInterface* udev_iface,
-                                         bool is_defer_discovery)
+                                         FileDescriptor* fd,
+                                         bool is_defer_discovery,
+                                         bool is_cache_test_mode)
     : udev_iface_(udev_iface),
+      fd_(fd),
+      is_cache_test_mode_(is_cache_test_mode),
       is_discovery_attempted_(false) {
-  CHECK(udev_iface);
+  CHECK(udev_iface && fd);
 
   // Ensure there's only one instance of this class.
   CHECK_EQ(num_instances_, static_cast<unsigned>(0));
   num_instances_++;
 
+  // Reset test signal flags.
+  ResetTestModeSignalingFlags();
+
   // If GPIO discovery not deferred, do it.
   if (!(is_defer_discovery || DiscoverGpios())) {
     LOG(WARNING) << "GPIO discovery failed";
@@ -57,17 +64,29 @@
   num_instances_--;
 }
 
-// TODO(garnold) currently, this function always returns false and avoids the
-// GPIO signaling protocol altogether; to be extended later.
 bool StandardGpioHandler::IsTestModeSignaled() {
   // Attempt GPIO discovery.
   if (!DiscoverGpios()) {
     LOG(WARNING) << "GPIO discovery failed";
   }
 
-  return false;
+  // Force a check if so requested.
+  if (!is_cache_test_mode_)
+    ResetTestModeSignalingFlags();
+
+  bool is_returning_cached = !is_first_check_;  // for logging purposes
+  if (is_first_check_) {
+    is_first_check_ = false;
+    DoTestModeSignalingProtocol();
+  }
+
+  LOG(INFO) << "result: " << (is_test_mode_ ? "test" : "normal") << " mode"
+            << (is_returning_cached ? " (cached)" : "")
+            << (is_handshake_completed_ ? "" : " (default)");
+  return is_test_mode_;
 }
 
+
 bool StandardGpioHandler::GpioChipUdevEnumHelper::SetupEnumFilters(
     udev_enumerate* udev_enum) {
   CHECK(udev_enum);
@@ -155,6 +174,23 @@
   return true;
 }
 
+StandardGpioHandler::GpioDirResetter::GpioDirResetter(
+    StandardGpioHandler* handler, GpioId id, GpioDir dir) :
+    do_reset_(false), handler_(handler), id_(id), dir_(dir) {
+  CHECK(handler);
+  CHECK_GE(id, 0);
+  CHECK_LT(id, kGpioIdMax);
+  CHECK_GE(dir, 0);
+  CHECK_LT(dir, kGpioDirMax);
+}
+
+StandardGpioHandler::GpioDirResetter::~GpioDirResetter() {
+  if (do_reset_ && !handler_->SetGpioDirection(id_, dir_)) {
+    LOG(WARNING) << "failed to reset direction of " << gpio_defs_[id_].name
+                 << " to " << gpio_dirs_[dir_];
+  }
+}
+
 
 bool StandardGpioHandler::InitUdevEnum(struct udev* udev,
                                        UdevEnumHelper* enum_helper) {
@@ -209,6 +245,12 @@
   return enum_helper->Finalize();
 }
 
+void StandardGpioHandler::ResetTestModeSignalingFlags() {
+  is_first_check_ = true;
+  is_handshake_completed_ = false;
+  is_test_mode_ = false;
+}
+
 bool StandardGpioHandler::DiscoverGpios() {
   if (is_discovery_attempted_)
     return true;
@@ -251,4 +293,289 @@
   return true;
 }
 
+bool StandardGpioHandler::OpenGpioFd(StandardGpioHandler::GpioId id,
+                                     const char* dev_name,
+                                     bool is_write) {
+  CHECK(id >= 0 && id < kGpioIdMax && dev_name);
+  string file_name = StringPrintf("%s/%s", gpios_[id].dev_path.c_str(),
+                                  dev_name);
+  if (!fd_->Open(file_name.c_str(), (is_write ? O_WRONLY : O_RDONLY))) {
+    const string err_str = StringPrintf("failed to open %s (%s) for %s",
+                                        file_name.c_str(), gpio_defs_[id].name,
+                                        (is_write ? "writing" : "reading"));
+    if (fd_->IsSettingErrno()) {
+      PLOG(ERROR) << err_str;
+    } else {
+      LOG(ERROR) << err_str;
+    }
+    return false;
+  }
+  return true;
+}
+
+bool StandardGpioHandler::SetGpio(StandardGpioHandler::GpioId id,
+                                  const char* dev_name, const char* entries[],
+                                  const int num_entries, int index) {
+  CHECK_GE(id, 0);
+  CHECK_LT(id, kGpioIdMax);
+  CHECK(dev_name);
+  CHECK(entries);
+  CHECK_GT(num_entries, 0);
+  CHECK_GE(index, 0);
+  CHECK_LT(index, num_entries);
+
+  // Open device for writing.
+  if (!OpenGpioFd(id, dev_name, true))
+    return false;
+  ScopedFileDescriptorCloser dev_fd_closer(fd_);
+
+  // Write a string corresponding to the requested output index to the GPIO
+  // device, appending a newline.
+  string output_str = entries[index];
+  output_str += '\n';
+  ssize_t write_len = fd_->Write(output_str.c_str(), output_str.length());
+  if (write_len != static_cast<ssize_t>(output_str.length())) {
+    if (write_len < 0) {
+      const string err_str = "failed to write to GPIO";
+      if (fd_->IsSettingErrno()) {
+        PLOG(ERROR) << err_str;
+      } else {
+        LOG(ERROR) << err_str;
+      }
+    } else {
+      LOG(ERROR) << "wrong number of bytes written (" << write_len
+                 << " instead of " << output_str.length() << ")";
+    }
+    return false;
+  }
+
+  // Close the device explicitly, returning the close result.
+  return fd_->Close();
+}
+
+bool StandardGpioHandler::GetGpio(StandardGpioHandler::GpioId id,
+                                  const char* dev_name, const char* entries[],
+                                  const int num_entries, int* index_p) {
+  CHECK_GE(id, 0);
+  CHECK_LT(id, kGpioIdMax);
+  CHECK(dev_name);
+  CHECK(entries);
+  CHECK_GT(num_entries, 0);
+  CHECK(index_p);
+
+  // Open device for reading.
+  if (!OpenGpioFd(id, dev_name, false))
+    return false;
+  ScopedFileDescriptorCloser dev_fd_closer(fd_);
+
+  // Read the GPIO device. We attempt to read more than the max number of
+  // characters expected followed by a newline, to ensure that we've indeed read
+  // all the data available on the device.
+  size_t max_entry_len = 0;
+  for (int i = 0; i < num_entries; i++) {
+    size_t entry_len = strlen(entries[i]);
+    if (entry_len > max_entry_len)
+      max_entry_len = entry_len;
+  }
+  max_entry_len++;  // account for trailing newline
+  size_t buf_len = max_entry_len + 1;  // room for excess char / null terminator
+  char buf[buf_len];
+  memset(buf, 0, buf_len);
+  ssize_t read_len = fd_->Read(buf, buf_len);
+  if (read_len < 0 || read_len > static_cast<ssize_t>(max_entry_len)) {
+    if (read_len < 0) {
+      const string err_str = "failed to read GPIO";
+      if (fd_->IsSettingErrno()) {
+        PLOG(ERROR) << err_str;
+      } else {
+        LOG(ERROR) << err_str;
+      }
+    } else {
+      LOG(ERROR) << "read too many bytes (" << read_len << ")";
+    }
+    return false;
+  }
+
+  // Remove trailing newline.
+  read_len--;
+  if (buf[read_len] != '\n') {
+    LOG(ERROR) << "read value missing trailing newline";
+    return false;
+  }
+  buf[read_len] = '\0';
+
+  // Identify and write GPIO status.
+  for (int i = 0; i < num_entries; i++)
+    if (!strcmp(entries[i], buf)) {
+      *index_p = i;
+      // Close the device explicitly, returning the close result.
+      return fd_->Close();
+    }
+
+  // Oops, unidentified reading...
+  LOG(ERROR) << "read unexpected value from GPIO (`" << buf << "')";
+  return false;
+}
+
+bool StandardGpioHandler::SetGpioDirection(StandardGpioHandler::GpioId id,
+                                           StandardGpioHandler::GpioDir dir) {
+  return SetGpio(id, "direction", gpio_dirs_, kGpioDirMax, dir);
+}
+
+bool StandardGpioHandler::GetGpioDirection(
+    StandardGpioHandler::GpioId id,
+    StandardGpioHandler::GpioDir* direction_p) {
+  return GetGpio(id, "direction", gpio_dirs_, kGpioDirMax,
+                 reinterpret_cast<int*>(direction_p));
+}
+
+bool StandardGpioHandler::SetGpioValue(StandardGpioHandler::GpioId id,
+                                       StandardGpioHandler::GpioVal value,
+                                       bool is_check_direction) {
+  // If so instructed, ensure that the GPIO is indeed in the output direction
+  // before attempting to write to it.
+  if (is_check_direction) {
+    GpioDir dir;
+    if (!(GetGpioDirection(id, &dir) && dir == kGpioDirOut)) {
+      LOG(ERROR) << "couldn't verify that GPIO is in the output direction "
+                    "prior to reading from it";
+      return false;
+    }
+  }
+
+  return SetGpio(id, "value", gpio_vals_, kGpioValMax, value);
+}
+
+bool StandardGpioHandler::GetGpioValue(StandardGpioHandler::GpioId id,
+                                       StandardGpioHandler::GpioVal* value_p,
+                                       bool is_check_direction) {
+  // If so instructed, ensure that the GPIO is indeed in the input direction
+  // before attempting to read from it.
+  if (is_check_direction) {
+    GpioDir dir;
+    if (!(GetGpioDirection(id, &dir) && dir == kGpioDirIn)) {
+      LOG(ERROR) << "couldn't verify that GPIO is in the input direction "
+                    "prior to reading from it";
+      return false;
+    }
+  }
+
+  return GetGpio(id, "value", gpio_vals_, kGpioValMax,
+                 reinterpret_cast<int*>(value_p));
+}
+
+bool StandardGpioHandler::DoTestModeSignalingProtocol() {
+  // The test mode signaling protocol is designed to provide a robust indication
+  // that a Chrome OS device is physically connected to a servo board in a lab
+  // setting. It is making very few assumptions about the soundness of the
+  // hardware, firmware and kernel driver implementation of the GPIO mechanism.
+  // In general, it is performing a three-way handshake between servo and the
+  // Chrome OS client, based on changes in the GPIO value readings. The
+  // client-side implementation does the following:
+  //
+  //  1. Check for an initial signal (0) on the input GPIO (dut_flaga).
+  //
+  //  2. Flip the signal (1 -> 0) on the output GPIO (dut_flagb).
+  //
+  //  3. Check for a flipped signal (1) on the input GPIO.
+  //
+  // TODO(garnold) the current implementation is based on sysfs access to GPIOs.
+  // We will likely change this to using a specialized in-kernel driver
+  // implementation, which would give us better performance and security
+  // guarantees.
+
+  LOG(INFO) << "attempting GPIO handshake";
+
+  const char* dutflaga_name = gpio_defs_[kGpioIdDutflaga].name;
+  const char* dutflagb_name = gpio_defs_[kGpioIdDutflagb].name;
+
+  // Flip GPIO direction, set it to "in".
+  // TODO(garnold) changing the GPIO direction back and forth is necessary for
+  // overcoming a firmware/kernel issue which causes the device to be in the
+  // "out" state whereas the kernel thinks it is in the "in" state.  This should
+  // be abandoned once the firmware and/or kernel driver have been fixed.
+  // Details here: http://code.google.com/p/chromium-os/issues/detail?id=27680
+  if (!(SetGpioDirection(kGpioIdDutflaga, kGpioDirOut) &&
+        SetGpioDirection(kGpioIdDutflaga, kGpioDirIn))) {
+    LOG(ERROR) << "failed to flip direction of input GPIO " << dutflaga_name;
+    return false;
+  }
+
+  // Peek input GPIO state.
+  GpioVal dutflaga_gpio_value;
+  if (!GetGpioValue(kGpioIdDutflaga, &dutflaga_gpio_value, true)) {
+    LOG(ERROR) << "failed to read input GPIO " << dutflaga_name;
+    return false;
+  }
+
+  // If initial handshake signal not received, abort.
+  if (dutflaga_gpio_value != kGpioValDown) {
+    LOG(INFO) << "input GPIO " << dutflaga_name
+              << " unset, terminating handshake";
+    is_handshake_completed_ = true;
+    return true;
+  }
+
+  // Initialize output GPIO to a default state.
+  // TODO(garnold) a similar workaround for possible driver/firmware glitches,
+  // we insist on flipping the direction of the GPIO prior to assuming it is in
+  // the "out" direction.
+  GpioDirResetter dutflagb_dir_resetter(this, kGpioIdDutflagb, kGpioDirIn);
+  if (!(SetGpioDirection(kGpioIdDutflagb, kGpioDirIn) &&
+        dutflagb_dir_resetter.set_do_reset(
+            SetGpioDirection(kGpioIdDutflagb, kGpioDirOut)) &&
+        SetGpioValue(kGpioIdDutflagb, kGpioValUp, false))) {
+    LOG(ERROR) << "failed to initialize output GPIO " << dutflagb_name;
+    return false;
+  }
+
+  // Wait, giving the receiving end enough time to sense the fall.
+  sleep(kServoOutputResponseWaitInSecs);
+
+  // Flip the output signal.
+  if (!SetGpioValue(kGpioIdDutflagb, kGpioValDown, false)) {
+    LOG(ERROR) << "failed to flip output GPIO " << dutflagb_name;
+    return false;
+  }
+
+  // Look for flipped input GPIO value, up to a preset timeout.
+  Time expires =
+      Time::Now() + TimeDelta::FromSeconds(kServoInputResponseTimeoutInSecs);
+  TimeDelta delay =
+      TimeDelta::FromMicroseconds(1000000 / kServoInputNumChecksPerSec);
+  bool is_first_response_check = true;
+  bool is_error = false;
+  while (Time::Now() < expires) {
+    if (is_first_response_check)
+      is_first_response_check = false;
+    else
+      usleep(delay.InMicroseconds());
+
+    // Read input GPIO.
+    if (!GetGpioValue(kGpioIdDutflaga, &dutflaga_gpio_value, true)) {
+      LOG(ERROR) << "failed to read input GPIO " << dutflaga_name;
+      is_error = true;
+      break;
+    }
+
+    // If dutflaga is now up (flipped), we got our signal!
+    if (dutflaga_gpio_value == kGpioValUp) {
+      is_test_mode_ = true;
+      break;
+    }
+  }
+
+  if (!is_error) {
+    if (is_test_mode_) {
+      is_handshake_completed_ = true;
+      LOG(INFO) << "GPIO handshake completed, test mode signaled";
+    } else {
+      LOG(INFO) << "timed out waiting for input GPIO " << dutflaga_name
+                << " to flip, terminating handshake";
+    }
+  }
+
+  return is_handshake_completed_;
+}
+
 }  // namespace chromeos_update_engine