| // Copyright 2020 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "typecd/port_manager.h" |
| |
| #include <string> |
| |
| #include <base/threading/platform_thread.h> |
| #include <base/time/time.h> |
| #include <base/logging.h> |
| #include <dbus/typecd/dbus-constants.h> |
| #include <re2/re2.h> |
| |
| namespace { |
| |
| // Give enough time for the EC to complete the ExitMode command. Calculated as |
| // follows: |
| // (tVDMWaitModeExit (50ms) * 3 possible signalling types (SOP, SOP', SOP'')) |
| // + 5 ms (typical ectool command) |
| // |
| // That gives us 155ms, so we double that to factor in scheduler and other |
| // delays. |
| constexpr uint32_t kExitModeWaitMs = 300; |
| |
| // Helper function to print the TypeCMode. |
| std::string ModeToString(typecd::TypeCMode mode) { |
| int val = static_cast<int>(mode); |
| switch (val) { |
| case 0: |
| return "DP"; |
| case 1: |
| return "TBT"; |
| case 2: |
| return "USB4"; |
| default: |
| return "none"; |
| } |
| } |
| |
| } // namespace |
| |
| namespace typecd { |
| |
| PortManager::PortManager() |
| : mode_entry_supported_(true), |
| supports_usb4_(true), |
| dbus_mgr_(nullptr), |
| features_client_(nullptr), |
| user_active_(false), |
| peripheral_data_access_(true), |
| port_num_previously_sink(-1), |
| metrics_(nullptr) {} |
| |
| void PortManager::OnPortAddedOrRemoved(const base::FilePath& path, |
| int port_num, |
| bool added) { |
| auto it = ports_.find(port_num); |
| if (added) { |
| if (it != ports_.end()) { |
| LOG(WARNING) << "Attempting to add an already added port."; |
| return; |
| } |
| |
| auto new_port = std::make_unique<Port>(path, port_num); |
| new_port->SetSupportsUSB4(supports_usb4_); |
| ports_.emplace(port_num, std::move(new_port)); |
| } else { |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Attempting to remove a non-existent port."; |
| return; |
| } |
| |
| ports_.erase(it); |
| } |
| } |
| |
| void PortManager::OnPartnerAddedOrRemoved(const base::FilePath& path, |
| int port_num, |
| bool added, |
| bool is_hotplug) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Partner add/remove attempted for non-existent port " |
| << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| if (added) { |
| port->AddPartner(path); |
| RunModeEntry(port_num); |
| ReportMetrics(port_num, is_hotplug); |
| } else { |
| port->RemovePartner(); |
| port->SetCurrentMode(TypeCMode::kNone); |
| port->CancelMetricsTask(); |
| } |
| } |
| |
| void PortManager::OnPartnerAltModeAddedOrRemoved(const base::FilePath& path, |
| int port_num, |
| bool added) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) |
| << "Partner alt mode add/remove attempted for non-existent port " |
| << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| port->AddRemovePartnerAltMode(path, added); |
| if (added) |
| RunModeEntry(port_num); |
| } |
| |
| void PortManager::OnCableAddedOrRemoved(const base::FilePath& path, |
| int port_num, |
| bool added) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Cable add/remove attempted for non-existent port " |
| << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| if (added) { |
| port->AddCable(path); |
| } else { |
| port->RemoveCable(); |
| } |
| } |
| |
| void PortManager::OnCablePlugAdded(const base::FilePath& path, int port_num) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Cable plug (SOP') add attempted for non-existent port " |
| << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| port->AddCablePlug(path); |
| RunModeEntry(port_num); |
| } |
| |
| void PortManager::OnCableAltModeAdded(const base::FilePath& path, |
| int port_num) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Cable alt mode add attempted for non-existent port " |
| << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| port->AddCableAltMode(path); |
| RunModeEntry(port_num); |
| } |
| |
| void PortManager::OnPartnerChanged(int port_num) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Partner change detected for non-existent port " |
| << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| port->PartnerChanged(); |
| RunModeEntry(port_num); |
| } |
| |
| void PortManager::OnPortChanged(int port_num) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Port change detected for non-existent port " << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| port->PortChanged(); |
| } |
| |
| void PortManager::OnScreenIsLocked() { |
| SetUserActive(false); |
| } |
| |
| void PortManager::OnScreenIsUnlocked() { |
| HandleUnlock(); |
| } |
| |
| void PortManager::OnSessionStarted() { |
| // Session started is handled similarly to "screen unlocked". |
| HandleUnlock(); |
| } |
| |
| void PortManager::OnSessionStopped() { |
| HandleSessionStopped(); |
| } |
| |
| void PortManager::HandleSessionStopped() { |
| if (!GetModeEntrySupported()) |
| return; |
| |
| SetUserActive(false); |
| for (auto const& x : ports_) { |
| Port* port = x.second.get(); |
| int port_num = x.first; |
| |
| // Since we've logged out, we can reset all expectations about active |
| // state during mode entry. |
| port->SetActiveStateOnModeEntry(GetUserActive()); |
| |
| // If the current mode is anything other than kTBT, we don't care about |
| // changing modes. |
| if (port->GetCurrentMode() != TypeCMode::kTBT) |
| continue; |
| |
| // If DP mode entry isn't supported, there is nothing left to do. |
| if (!port->CanEnterDPAltMode(nullptr)) |
| continue; |
| |
| // First try exiting the alt mode. |
| if (ec_util_->ExitMode(port_num)) { |
| port->SetCurrentMode(TypeCMode::kNone); |
| LOG(INFO) << "Exited TBT mode on port " << port_num; |
| } else { |
| LOG(ERROR) << "Attempt to call ExitMode failed for port " << port_num; |
| continue; |
| } |
| |
| base::PlatformThread::Sleep(base::Milliseconds(kExitModeWaitMs)); |
| |
| // Now run mode entry again. |
| RunModeEntry(port_num); |
| } |
| } |
| |
| void PortManager::HandleUnlock() { |
| if (!GetModeEntrySupported()) |
| return; |
| |
| if (features_client_) |
| SetPeripheralDataAccess(features_client_->GetPeripheralDataAccessEnabled()); |
| |
| SetUserActive(true); |
| for (auto const& x : ports_) { |
| Port* port = x.second.get(); |
| int port_num = x.first; |
| // If the current mode is anything other than DP, we don't care about |
| // changing modes. |
| if (port->GetCurrentMode() != TypeCMode::kDP) |
| continue; |
| |
| // If TBT mode entry isn't supported, there is nothing left to do. |
| if (port->CanEnterTBTCompatibilityMode() != ModeEntryResult::kSuccess) |
| continue; |
| |
| // If peripheral data access is disabled, we shouldn't switch modes at all. |
| if (!GetPeripheralDataAccess()) |
| continue; |
| |
| // If the port had initially entered the mode during an unlocked state, |
| // we shouldn't change modes now. Doing so will abruptly kick storage |
| // devices off the peripheral without a safe unmount. |
| if (port->GetActiveStateOnModeEntry()) |
| continue; |
| |
| // First try exiting the alt mode. |
| if (ec_util_->ExitMode(port_num)) { |
| port->SetCurrentMode(TypeCMode::kNone); |
| LOG(INFO) << "Exited DP mode on port " << port_num; |
| } else { |
| LOG(ERROR) << "Attempt to call ExitMode failed for port " << port_num; |
| continue; |
| } |
| |
| base::PlatformThread::Sleep(base::Milliseconds(kExitModeWaitMs)); |
| |
| // Now run mode entry again. |
| RunModeEntry(port_num); |
| } |
| } |
| |
| void PortManager::RunModeEntry(int port_num) { |
| if (!ec_util_) { |
| LOG(ERROR) << "No EC Util implementation registered, mode entry aborted."; |
| return; |
| } |
| |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Mode entry attempted for non-existent port " << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| |
| if (port->GetDataRole() != DataRole::kHost) { |
| LOG(WARNING) << "Can't enter mode; data role is not DFP on port " |
| << port_num; |
| return; |
| } |
| |
| if (!port->IsPartnerDiscoveryComplete()) { |
| LOG(INFO) << "Can't enter mode; partner discovery not complete for port " |
| << port_num; |
| return; |
| } |
| |
| if (!port->IsCableDiscoveryComplete()) { |
| LOG(INFO) << "Can't enter mode; cable discovery not complete for port " |
| << port_num; |
| return; |
| } |
| |
| if (port->GetCurrentMode() != TypeCMode::kNone) { |
| LOG(INFO) << "Mode entry already executed for port " << port_num |
| << ", mode: " << ModeToString(port->GetCurrentMode()); |
| return; |
| } |
| |
| if (!GetModeEntrySupported()) { |
| if (!dbus_mgr_) |
| return; |
| |
| // If mode entry is not attempted because the AP cannot enter modes, still |
| // check for cable notifications. |
| bool invalid_dpalt_cable = false; |
| bool can_enter_dp_alt_mode = port->CanEnterDPAltMode(&invalid_dpalt_cable); |
| if (can_enter_dp_alt_mode && invalid_dpalt_cable) |
| dbus_mgr_->NotifyCableWarning(CableWarningType::kInvalidDpCable); |
| |
| return; |
| } |
| |
| // Send TBT device-connected notification. |
| // While we can probably optimize this to avoid the repeat CanEnter* calls, we |
| // handle the notification calls ahead, in order to prevent the logic from |
| // becoming difficult to follow. |
| if (dbus_mgr_) { |
| if (port->CanEnterTBTCompatibilityMode() == ModeEntryResult::kSuccess) { |
| auto notif = port->CanEnterDPAltMode(nullptr) |
| ? DeviceConnectedType::kThunderboltDp |
| : DeviceConnectedType::kThunderboltOnly; |
| dbus_mgr_->NotifyConnected(notif); |
| } |
| } |
| |
| port->SetActiveStateOnModeEntry(GetUserActive()); |
| |
| if (features_client_) |
| SetPeripheralDataAccess(features_client_->GetPeripheralDataAccessEnabled()); |
| |
| // If the host supports USB4 and we can enter USB4 in this partner, do so. |
| auto can_enter_usb4 = port->CanEnterUSB4(); |
| if (can_enter_usb4 == ModeEntryResult::kSuccess) { |
| if (ec_util_->EnterMode(port_num, TypeCMode::kUSB4)) { |
| port->SetCurrentMode(TypeCMode::kUSB4); |
| LOG(INFO) << "Entered USB4 mode on port " << port_num; |
| } else { |
| LOG(ERROR) << "Attempt to call Enter USB4 failed for port " << port_num; |
| } |
| |
| // If the cable limits USB speed, warn the user. |
| if (port->CableLimitingUSBSpeed(false)) { |
| LOG(INFO) << "Cable limiting USB speed on port " << port_num; |
| if (dbus_mgr_) |
| dbus_mgr_->NotifyCableWarning(CableWarningType::kSpeedLimitingCable); |
| } |
| |
| return; |
| } |
| |
| auto can_enter_thunderbolt = port->CanEnterTBTCompatibilityMode(); |
| if (can_enter_thunderbolt == ModeEntryResult::kSuccess) { |
| // Check if DP alt mode can be entered. If so: |
| // - If the user is not active: enter DP. |
| // - If the user is active: if peripheral data access is disabled, enter DP, |
| // else enter TBT. |
| // |
| // If DP alt mode cannot be entered, proceed to enter TBT in all cases. |
| TypeCMode cur_mode = TypeCMode::kTBT; |
| if (port->CanEnterDPAltMode(nullptr) && |
| (!GetUserActive() || (GetUserActive() && !GetPeripheralDataAccess()))) { |
| cur_mode = TypeCMode::kDP; |
| LOG(INFO) << "Not entering TBT compat mode since user_active: " |
| << GetUserActive() |
| << ", peripheral data access: " << GetPeripheralDataAccess() |
| << ", port " << port_num; |
| } |
| |
| if (ec_util_->EnterMode(port_num, cur_mode)) { |
| port->SetCurrentMode(cur_mode); |
| LOG(INFO) << "Entered " << ModeToString(cur_mode) << " mode on port " |
| << port_num; |
| } else { |
| LOG(ERROR) << "Attempt to call enter " << ModeToString(cur_mode) |
| << " failed for port " << port_num; |
| } |
| |
| if (can_enter_usb4 == ModeEntryResult::kCableError) { |
| // If TBT is entered due to a USB4 cable error, warn the user. |
| LOG(WARNING) << "USB4 partner with TBT cable on port " << port_num; |
| if (dbus_mgr_) |
| dbus_mgr_->NotifyCableWarning( |
| CableWarningType::kInvalidUSB4ValidTBTCable); |
| } else if (port->CableLimitingUSBSpeed(true)) { |
| // Cable limits the speed of TBT3 partner. |
| LOG(INFO) << "Cable limiting USB speed on port " << port_num; |
| if (dbus_mgr_) |
| dbus_mgr_->NotifyCableWarning(CableWarningType::kSpeedLimitingCable); |
| } |
| |
| return; |
| } |
| |
| bool invalid_dpalt_cable = false; |
| if (port->CanEnterDPAltMode(&invalid_dpalt_cable)) { |
| if (ec_util_->EnterMode(port_num, TypeCMode::kDP)) { |
| port->SetCurrentMode(TypeCMode::kDP); |
| LOG(INFO) << "Entered DP mode on port " << port_num; |
| } else { |
| LOG(ERROR) << "Attempt to call Enter DP failed for port " << port_num; |
| } |
| } |
| |
| // CableWarningType to track possible cable notifications. |
| CableWarningType cable_warning = CableWarningType::kOther; |
| if (can_enter_usb4 == ModeEntryResult::kCableError) { |
| cable_warning = CableWarningType::kInvalidUSB4Cable; |
| LOG(WARNING) << "USB4 partner with incompatible cable on port " << port_num; |
| } else if (can_enter_thunderbolt == ModeEntryResult::kCableError) { |
| cable_warning = CableWarningType::kInvalidTBTCable; |
| LOG(WARNING) << "TBT partner with incompatible cable on port " << port_num; |
| } else if (invalid_dpalt_cable) { |
| cable_warning = CableWarningType::kInvalidDpCable; |
| LOG(WARNING) << "DPAltMode partner with incompatible cable on port " |
| << port_num; |
| } else if (port->CableLimitingUSBSpeed(false)) { |
| cable_warning = CableWarningType::kSpeedLimitingCable; |
| LOG(INFO) << "Cable limiting USB speed on port " << port_num; |
| } |
| |
| // Notify user of potential cable issue. |
| if (dbus_mgr_ && cable_warning != CableWarningType::kOther) |
| dbus_mgr_->NotifyCableWarning(cable_warning); |
| |
| return; |
| } |
| |
| void PortManager::ReportMetrics(int port_num, bool is_hotplug) { |
| if (!metrics_) |
| return; |
| |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) << "Metrics reporting attempted for non-existent port " |
| << port_num; |
| return; |
| } |
| |
| auto port = it->second.get(); |
| port->EnqueueMetricsTask(metrics_, GetModeEntrySupported()); |
| |
| if (port->GetPanel() != Panel::kUnknown) { |
| metrics_->ReportPartnerLocation( |
| GetPartnerLocationMetric(port_num, is_hotplug)); |
| if (port->GetPowerRole() == PowerRole::kSink) |
| metrics_->ReportPowerSourceLocation( |
| GetPowerSourceLocationMetric(port_num)); |
| } |
| } |
| |
| PartnerLocationMetric PortManager::GetPartnerLocationMetric(int port_num, |
| bool is_hotplug) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) |
| << "Port location metric reporting attempted for non-existent port " |
| << port_num; |
| return PartnerLocationMetric::kOther; |
| } |
| auto port = it->second.get(); |
| |
| PartnerLocationMetric ret = PartnerLocationMetric::kOther; |
| Panel panel = port->GetPanel(); |
| int num_ports_already_in_use = 0; |
| Panel panel_already_in_use = Panel::kUnknown; |
| int num_available_ports_on_left = 0; |
| int num_available_ports_on_right = 0; |
| |
| if (!is_hotplug && panel == Panel::kLeft) { |
| ret = PartnerLocationMetric::kLeftColdplugged; |
| goto end; |
| } else if (!is_hotplug && panel == Panel::kRight) { |
| ret = PartnerLocationMetric::kRightColdplugged; |
| goto end; |
| } |
| |
| for (auto it = ports_.begin(); it != ports_.end(); it++) { |
| if (it->first == port_num) |
| continue; |
| auto port_to_check = it->second.get(); |
| if (port_to_check->HasPartner()) { |
| num_ports_already_in_use++; |
| // It is okay to overwrite panel_already_in_use because when multiple |
| // partners are in use, the metric won't track previous port connections. |
| panel_already_in_use = port_to_check->GetPanel(); |
| } else if (port_to_check->GetPanel() == Panel::kLeft) { |
| num_available_ports_on_left++; |
| } else if (port_to_check->GetPanel() == Panel::kRight) { |
| num_available_ports_on_right++; |
| } |
| } |
| |
| if (panel == Panel::kLeft) { |
| if (num_available_ports_on_right == 0) |
| ret = PartnerLocationMetric::kUserHasNoChoice; |
| else if (num_ports_already_in_use == 0) |
| ret = PartnerLocationMetric::kLeftFirst; |
| else if (num_ports_already_in_use == 1 && |
| panel_already_in_use == Panel::kLeft) |
| ret = PartnerLocationMetric::kLeftSecondSameSideWithFirst; |
| else if (num_ports_already_in_use == 1 && |
| panel_already_in_use == Panel::kRight) |
| ret = PartnerLocationMetric::kLeftSecondOppositeSideToFirst; |
| else if (num_ports_already_in_use >= 2) |
| ret = PartnerLocationMetric::kLeftThirdOrLater; |
| } else if (panel == Panel::kRight) { |
| if (num_available_ports_on_left == 0) |
| ret = PartnerLocationMetric::kUserHasNoChoice; |
| else if (num_ports_already_in_use == 0) |
| ret = PartnerLocationMetric::kRightFirst; |
| else if (num_ports_already_in_use == 1 && |
| panel_already_in_use == Panel::kRight) |
| ret = PartnerLocationMetric::kRightSecondSameSideWithFirst; |
| else if (num_ports_already_in_use == 1 && |
| panel_already_in_use == Panel::kLeft) |
| ret = PartnerLocationMetric::kRightSecondOppositeSideToFirst; |
| else if (num_ports_already_in_use >= 2) |
| ret = PartnerLocationMetric::kRightThirdOrLater; |
| } |
| |
| end: |
| return ret; |
| } |
| |
| PowerSourceLocationMetric PortManager::GetPowerSourceLocationMetric( |
| int port_num) { |
| auto it = ports_.find(port_num); |
| if (it == ports_.end()) { |
| LOG(WARNING) |
| << "Port location metric reporting attempted for non-existent port " |
| << port_num; |
| return PowerSourceLocationMetric::kOther; |
| } |
| auto port = it->second.get(); |
| |
| Panel panel = port->GetPanel(); |
| int num_ports_on_left = 0; |
| int num_ports_on_right = 0; |
| Panel panel_prev = Panel::kUnknown; |
| |
| for (auto it = ports_.begin(); it != ports_.end(); it++) { |
| auto port_to_check = it->second.get(); |
| if (port_to_check->GetPanel() == Panel::kLeft) { |
| num_ports_on_left++; |
| } else if (port_to_check->GetPanel() == Panel::kRight) { |
| num_ports_on_right++; |
| } |
| } |
| |
| it = ports_.find(port_num_previously_sink); |
| if (it != ports_.end()) { |
| auto port_prev = it->second.get(); |
| panel_prev = port_prev->GetPanel(); |
| } |
| |
| PowerSourceLocationMetric ret = PowerSourceLocationMetric::kOther; |
| if (panel == Panel::kLeft) { |
| if (num_ports_on_right == 0) |
| ret = PowerSourceLocationMetric::kUserHasNoChoice; |
| else if (panel_prev == Panel::kUnknown) |
| ret = PowerSourceLocationMetric::kLeftFirst; |
| else if (panel_prev == Panel::kRight) |
| ret = PowerSourceLocationMetric::kLeftSwitched; |
| else if (panel_prev == Panel::kLeft) |
| ret = PowerSourceLocationMetric::kLeftConstant; |
| } else if (panel == Panel::kRight) { |
| if (num_ports_on_left == 0) |
| ret = PowerSourceLocationMetric::kUserHasNoChoice; |
| else if (panel_prev == Panel::kUnknown) |
| ret = PowerSourceLocationMetric::kRightFirst; |
| else if (panel_prev == Panel::kLeft) |
| ret = PowerSourceLocationMetric::kRightSwitched; |
| else if (panel_prev == Panel::kRight) |
| ret = PowerSourceLocationMetric::kRightConstant; |
| } |
| |
| port_num_previously_sink = port_num; |
| return ret; |
| } |
| |
| } // namespace typecd |