control: command line tool to retrieve fan status (set/resume functions)

This is part three of a multipart commit to create a fan control command
line utility to report service status, show target/actual RPM/PWM info,
and manually control single fans. This commit contains the set/resume
commands. Further functionality will come in subsequent commits.

The resume command is a shortcut to start phosphor-fan-control@0 systemd
service, since most workflows require stopping it to allow control of
individual fans. Once manual intervention is done, it can be resumed.

root@p10bmc:~# systemctl stop phosphor-fan-control@0.service
root@p10bmc:~# fanctl resume
root@p10bmc:~# systemctl status phosphor-fan-control@0.service
root@p10bmc:~# <output indicates service is running>

For set/get functions, here is a usage example:

root@p10bmc:~# fanctl set 10000
root@p10bmc:~# fanctl set 2000 fan1 fan3_0
root@p10bmc:~# fanctl get

TARGET SENSOR    TARGET(RPM)   FEEDBACK SENSOR   FEEDBACK(RPMS)
===============================================================
fan0_0                 10000            fan0_0            10000
                                        fan0_1            13591
fan1_0                  2000            fan1_0             1991
                                        fan1_1             2000
fan2_0                 10000            fan2_0            10000
                                        fan2_1            13591
fan3_0                  2000            fan3_0             1991
                                        fan3_1             2000
fan4_0                 10000            fan4_0            10000
                                        fan4_1            10091
fan5_0                 10000            fan5_0            10000
                                        fan5_1            10021

Signed-off-by: Mike Capps <mikepcapps@gmail.com>
Change-Id: Ib2b2f7433701e0178fcb8d2223e452b541ea5b0c
diff --git a/control/Makefile.am b/control/Makefile.am
index 2ac6d4a..33c89d2 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -55,14 +55,13 @@
 fanctl_SOURCES = \
 	fanctl.cpp
 
-fanctl_CXXFLAGS = \
-	$(SDBUSPLUS_CFLAGS) \
-	-flto
-
 fanctl_LDADD = \
 	$(SDBUSPLUS_LIBS) \
 	$(FMT_LIBS)
 
+fanctl_CXXFLAGS = \
+	$(SDBUSPLUS_CFLAGS) \
+	-flto
 
 
 if WANT_JSON_CONTROL
diff --git a/control/fanctl.cpp b/control/fanctl.cpp
index 0061094..c701817 100644
--- a/control/fanctl.cpp
+++ b/control/fanctl.cpp
@@ -24,6 +24,19 @@
 
 using SDBusPlus = phosphor::fan::util::SDBusPlus;
 
+constexpr auto phosphorServiceName = "phosphor-fan-control@0.service";
+constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
+constexpr auto systemdPath = "/org/freedesktop/systemd1";
+constexpr auto systemdService = "org.freedesktop.systemd1";
+
+enum
+{
+    FAN_NAMES = 0,
+    PATH_MAP = 1,
+    IFACES = 2,
+    METHOD = 3
+};
+
 /**
  * @function extracts fan name from dbus path string (last token where
  * delimiter is the / character), with proper bounds checking.
@@ -164,13 +177,9 @@
                    std::string, std::string, sdbusplus::message::object_path,
                    uint32_t, std::string, sdbusplus::message::object_path>;
 
-    constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
-    constexpr auto systemdPath = "/org/freedesktop/systemd1";
-    constexpr auto systemdService = "org.freedesktop.systemd1";
-
     std::array<std::string, 6> ret;
 
-    std::vector<std::string> services{"phosphor-fan-control@0.service"};
+    std::vector<std::string> services{phosphorServiceName};
 
     try
     {
@@ -233,7 +242,7 @@
     using std::setw;
 
     auto busData = loadDBusData();
-    auto& method = std::get<3>(busData);
+    auto& method = std::get<METHOD>(busData);
 
     std::string property;
 
@@ -254,9 +263,9 @@
     cout << "==============================================================="
          << endl;
 
-    auto& fanNames{std::get<0>(busData)};
-    auto& pathMap{std::get<1>(busData)};
-    auto& interfaces{std::get<2>(busData)};
+    auto& fanNames{std::get<FAN_NAMES>(busData)};
+    auto& pathMap{std::get<PATH_MAP>(busData)};
+    auto& interfaces{std::get<IFACES>(busData)};
 
     for (auto& fan : fanNames)
     {
@@ -349,10 +358,10 @@
 
     auto busData = loadDBusData();
 
-    auto& fanNames{std::get<0>(busData)};
-    auto& pathMap{std::get<1>(busData)};
-    auto& interfaces{std::get<2>(busData)};
-    auto& method = std::get<3>(busData);
+    auto& fanNames{std::get<FAN_NAMES>(busData)};
+    auto& pathMap{std::get<PATH_MAP>(busData)};
+    auto& interfaces{std::get<IFACES>(busData)};
+    auto& method = std::get<METHOD>(busData);
 
     std::string property;
 
@@ -398,11 +407,105 @@
 }
 
 /**
+ * @function set fan[s] to a target RPM
+ */
+void set(uint64_t target, std::vector<std::string> fanList)
+{
+    auto busData = loadDBusData();
+
+    auto& bus{SDBusPlus::getBus()};
+    auto& pathMap{std::get<PATH_MAP>(busData)};
+    auto& interfaces{std::get<IFACES>(busData)};
+    auto& method = std::get<METHOD>(busData);
+
+    std::string ifaceType(method == "RPM" ? "FanSpeed" : "FanPwm");
+
+    // stop the fan-control service
+    auto retval = SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>(
+        systemdService, systemdPath, systemdMgrIface, "StopUnit",
+        phosphorServiceName, "replace");
+
+    if (fanList.size() == 0)
+    {
+        fanList = std::get<FAN_NAMES>(busData);
+    }
+
+    for (auto& fan : fanList)
+    {
+        try
+        {
+            auto paths(pathMap["tach"].find(fan));
+
+            if (pathMap["tach"].end() == paths)
+            {
+                // try again, maybe it was a sensor name instead of a fan name
+                for (const auto& [fanName, sensors] : pathMap["tach"])
+                {
+                    for (const auto& path : sensors)
+                    {
+                        std::string sensor(path.substr(path.rfind("/")));
+
+                        if (sensor.size() > 0)
+                        {
+                            sensor = sensor.substr(1);
+
+                            if (sensor == fan)
+                            {
+                                paths = pathMap["tach"].find(fanName);
+
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (pathMap["tach"].end() == paths)
+            {
+                std::cout << "Could not find tach path for fan: " << fan
+                          << std::endl;
+                continue;
+            }
+
+            // set the target RPM
+            SDBusPlus::setProperty<uint64_t>(bus, paths->second[0],
+                                             interfaces[ifaceType], "Target",
+                                             std::move(target));
+        }
+        catch (const phosphor::fan::util::DBusPropertyError& e)
+        {
+            std::cerr << "Cannot set target rpm for " << fan
+                      << " caught D-Bus exception: " << e.what() << std::endl;
+        }
+    }
+}
+
+/**
+ * @function restart fan-control to allow it to manage fan speeds
+ */
+void resume()
+{
+    try
+    {
+        auto retval =
+            SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>(
+                systemdService, systemdPath, systemdMgrIface, "StartUnit",
+                phosphorServiceName, "replace");
+    }
+    catch (const phosphor::fan::util::DBusMethodError& e)
+    {
+        std::cerr << "Unable to start fan control: " << e.what() << std::endl;
+    }
+}
+
+/**
  * @function main entry point for the application
  */
 int main(int argc, char* argv[])
 {
     auto rc = 0;
+    uint64_t target{0U};
+    std::vector<std::string> fanList;
 
     try
     {
@@ -417,17 +520,16 @@
         // This represents the command given
         auto commands = app.add_option_group("Commands");
 
-        // Configure method
+        // status method
         auto cmdStatus = commands->add_subcommand(
             "status",
             "Get the fan tach targets/values and fan-control service status");
-
         cmdStatus->set_help_flag(
             "-h, --help", "Prints fan target/tach readings, present/functional "
                           "states, and fan-monitor/BMC/Power service status");
         cmdStatus->require_option(0);
 
-        // Get method
+        // get method
         auto cmdGet = commands->add_subcommand(
             "get",
             "Get the current fan target and feedback speeds for all rotors");
@@ -436,16 +538,44 @@
             "Get the current fan target and feedback speeds for all rotors");
         cmdGet->require_option(0);
 
+        // set method
+        auto cmdSet = commands->add_subcommand(
+            "set", "Set fan(s) target speed for all rotors");
+        cmdSet->add_option("target", target, "RPM/PWM target to set the fans");
+        cmdSet->add_option("fan list", fanList,
+                           "[optional] list of fans to set target RPM");
+
+        std::string strHelp = "Resume running phosphor-fan-control";
+        auto cmdResume = commands->add_subcommand("resume", strHelp);
+        cmdResume->set_help_flag("-h, --help", strHelp);
+        cmdResume->require_option(0);
+
+        auto setHelp(R"(set <TARGET> [\"TARGET SENSOR LIST\"]
+      <TARGET>
+          - RPM/PWM target to set the fans
+[TARGET SENSOR LIST]
+  - list of target sensors to set)");
+        cmdSet->set_help_flag("-h, --help", setHelp);
+        cmdSet->require_option();
+
         CLI11_PARSE(app, argc, argv);
 
         if (app.got_subcommand("status"))
         {
             status();
         }
+        else if (app.got_subcommand("set"))
+        {
+            set(target, fanList);
+        }
         else if (app.got_subcommand("get"))
         {
             get();
         }
+        else if (app.got_subcommand("resume"))
+        {
+            resume();
+        }
     }
     catch (const std::exception& e)
     {