DbusPidZone: Allow per-zone setpoint files during tuning

In tuning mode, in addition to the overall setpoint file,
also check for the existence of per-zone setpoint files.

If they exist, and contain a parseable RPM number,
they will be used. If both per-zone and overall files exist,
the per-zone will override the overall, appropriately for each zone.

Better error checking has been added,
detecting common user mistake of using PWM instead of RPM,
throttling error messages to avoid repetitive output.

Tested: It worked, here's an example...
echo 5000 > /etc/thermal.d/setpoint.zone0
echo 7000 > /etc/thermal.d/setpoint
echo 9000 > /etc/thermal.d/setpoint.zone2
Zone 0 will try for 5000 RPM, Zone 2 will try for 9000 RPM,
and Zone 1 (and all other zones) will try for 7000 RPM.

Signed-off-by: Josh Lehan <krellan@google.com>
Change-Id: Ic78d0fd2a0c32308356a0e2c7b03453416467c5b
diff --git a/pid/zone.cpp b/pid/zone.cpp
index 7f4a903..441031a 100644
--- a/pid/zone.cpp
+++ b/pid/zone.cpp
@@ -31,14 +31,44 @@
 #include <fstream>
 #include <iostream>
 #include <memory>
+#include <sstream>
 #include <string>
 
-namespace pid_control
-{
-
 using tstamp = std::chrono::high_resolution_clock::time_point;
 using namespace std::literals::chrono_literals;
 
+// Enforces minimum duration between events
+// Rreturns true if event should be allowed, false if disallowed
+bool allowThrottle(const tstamp& now, const std::chrono::seconds& pace)
+{
+    static tstamp then;
+    static bool first = true;
+
+    if (first)
+    {
+        // Special case initialization
+        then = now;
+        first = false;
+
+        // Initialization, always allow
+        return true;
+    }
+
+    auto elapsed = now - then;
+    if (elapsed < pace)
+    {
+        // Too soon since last time, disallow
+        return false;
+    }
+
+    // It has been long enough, allow
+    then = now;
+    return true;
+}
+
+namespace pid_control
+{
+
 double DbusPidZone::getMaxSetPointRequest(void) const
 {
     return _maximumSetPoint;
@@ -120,6 +150,58 @@
     _thermalInputs.push_back(therm);
 }
 
+// Updates desired RPM setpoint from optional text file
+// Returns true if rpmValue updated, false if left unchanged
+static bool fileParseRpm(const std::string& fileName, double& rpmValue)
+{
+    static constexpr std::chrono::seconds throttlePace{3};
+
+    std::string errText;
+
+    try
+    {
+        std::ifstream ifs;
+        ifs.open(fileName);
+        if (ifs)
+        {
+            int value;
+            ifs >> value;
+
+            if (value <= 0)
+            {
+                errText = "File content could not be parsed to a number";
+            }
+            else if (value <= 100)
+            {
+                errText = "File must contain RPM value, not PWM value";
+            }
+            else
+            {
+                rpmValue = static_cast<double>(value);
+                return true;
+            }
+        }
+    }
+    catch (const std::exception& e)
+    {
+        errText = "Exception: ";
+        errText += e.what();
+    }
+
+    // The file is optional, intentionally not an error if file not found
+    if (!(errText.empty()))
+    {
+        tstamp now = std::chrono::high_resolution_clock::now();
+        if (allowThrottle(now, throttlePace))
+        {
+            std::cerr << "Unable to read from '" << fileName << "': " << errText
+                      << "\n";
+        }
+    }
+
+    return false;
+}
+
 void DbusPidZone::determineMaxSetPointRequest(void)
 {
     double max = 0;
@@ -151,24 +233,15 @@
          * fan sensors and one large fan PID for all the fans.
          */
         static constexpr auto setpointpath = "/etc/thermal.d/setpoint";
-        try
-        {
-            std::ifstream ifs;
-            ifs.open(setpointpath);
-            if (ifs.good())
-            {
-                int value;
-                ifs >> value;
 
-                /* expecting RPM setpoint, not pwm% */
-                max = static_cast<double>(value);
-            }
-        }
-        catch (const std::exception& e)
-        {
-            /* This exception is uninteresting. */
-            std::cerr << "Unable to read from '" << setpointpath << "'\n";
-        }
+        fileParseRpm(setpointpath, max);
+
+        // Allow per-zone setpoint files to override overall setpoint file
+        std::ostringstream zoneSuffix;
+        zoneSuffix << ".zone" << _zoneId;
+        std::string zoneSetpointPath = setpointpath + zoneSuffix.str();
+
+        fileParseRpm(zoneSetpointPath, max);
     }
 
     _maximumSetPoint = max;