1 /**
2  * Copyright 2019 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include "pid/buildjson.hpp"
18 
19 #include "conf.hpp"
20 
21 #include <nlohmann/json.hpp>
22 
23 #include <iostream>
24 #include <map>
25 #include <tuple>
26 
27 namespace pid_control
28 {
29 
30 using json = nlohmann::json;
31 
32 namespace conf
33 {
34 void from_json(const json& j, conf::ControllerInfo& c)
35 {
36     j.at("type").get_to(c.type);
37     j.at("inputs").get_to(c.inputs);
38     j.at("setpoint").get_to(c.setpoint);
39 
40     /* TODO: We need to handle parsing other PID controller configurations.
41      * We can do that by checking for different keys and making the decision
42      * accordingly.
43      */
44     auto p = j.at("pid");
45 
46     auto positiveHysteresis = p.find("positiveHysteresis");
47     auto negativeHysteresis = p.find("negativeHysteresis");
48     auto derivativeCoeff = p.find("derivativeCoeff");
49     auto positiveHysteresisValue = 0.0;
50     auto negativeHysteresisValue = 0.0;
51     auto derivativeCoeffValue = 0.0;
52     if (positiveHysteresis != p.end())
53     {
54         positiveHysteresis->get_to(positiveHysteresisValue);
55     }
56     if (negativeHysteresis != p.end())
57     {
58         negativeHysteresis->get_to(negativeHysteresisValue);
59     }
60     if (derivativeCoeff != p.end())
61     {
62         derivativeCoeff->get_to(derivativeCoeffValue);
63     }
64 
65     if (c.type != "stepwise")
66     {
67         p.at("samplePeriod").get_to(c.pidInfo.ts);
68         p.at("proportionalCoeff").get_to(c.pidInfo.proportionalCoeff);
69         p.at("integralCoeff").get_to(c.pidInfo.integralCoeff);
70         p.at("feedFwdOffsetCoeff").get_to(c.pidInfo.feedFwdOffset);
71         p.at("feedFwdGainCoeff").get_to(c.pidInfo.feedFwdGain);
72         p.at("integralLimit_min").get_to(c.pidInfo.integralLimit.min);
73         p.at("integralLimit_max").get_to(c.pidInfo.integralLimit.max);
74         p.at("outLim_min").get_to(c.pidInfo.outLim.min);
75         p.at("outLim_max").get_to(c.pidInfo.outLim.max);
76         p.at("slewNeg").get_to(c.pidInfo.slewNeg);
77         p.at("slewPos").get_to(c.pidInfo.slewPos);
78 
79         // Unlike other coefficients, treat derivativeCoeff as an optional
80         // parameter, as support for it is fairly new, to avoid breaking
81         // existing configurations in the field that predate it.
82         c.pidInfo.positiveHysteresis = positiveHysteresisValue;
83         c.pidInfo.negativeHysteresis = negativeHysteresisValue;
84         c.pidInfo.derivativeCoeff = derivativeCoeffValue;
85     }
86     else
87     {
88         p.at("samplePeriod").get_to(c.stepwiseInfo.ts);
89         p.at("isCeiling").get_to(c.stepwiseInfo.isCeiling);
90 
91         for (size_t i = 0; i < ec::maxStepwisePoints; i++)
92         {
93             c.stepwiseInfo.reading[i] =
94                 std::numeric_limits<double>::quiet_NaN();
95             c.stepwiseInfo.output[i] = std::numeric_limits<double>::quiet_NaN();
96         }
97 
98         auto reading = p.find("reading");
99         if (reading != p.end())
100         {
101             auto r = p.at("reading");
102             for (size_t i = 0; i < ec::maxStepwisePoints; i++)
103             {
104                 auto n = r.find(std::to_string(i));
105                 if (n != r.end())
106                 {
107                     r.at(std::to_string(i)).get_to(c.stepwiseInfo.reading[i]);
108                 }
109             }
110         }
111 
112         auto output = p.find("output");
113         if (output != p.end())
114         {
115             auto o = p.at("output");
116             for (size_t i = 0; i < ec::maxStepwisePoints; i++)
117             {
118                 auto n = o.find(std::to_string(i));
119                 if (n != o.end())
120                 {
121                     o.at(std::to_string(i)).get_to(c.stepwiseInfo.output[i]);
122                 }
123             }
124         }
125 
126         c.stepwiseInfo.positiveHysteresis = positiveHysteresisValue;
127         c.stepwiseInfo.negativeHysteresis = negativeHysteresisValue;
128     }
129 }
130 } // namespace conf
131 
132 std::pair<std::map<int64_t, conf::PIDConf>, std::map<int64_t, conf::ZoneConfig>>
133     buildPIDsFromJson(const json& data)
134 {
135     // zone -> pids
136     std::map<int64_t, conf::PIDConf> pidConfig;
137     // zone -> configs
138     std::map<int64_t, conf::ZoneConfig> zoneConfig;
139 
140     /* TODO: if zones is empty, that's invalid. */
141     auto zones = data["zones"];
142     for (const auto& zone : zones)
143     {
144         int64_t id;
145         conf::PIDConf thisZone;
146         conf::ZoneConfig thisZoneConfig;
147 
148         /* TODO: using at() throws a specific exception we can catch */
149         id = zone["id"];
150         thisZoneConfig.minThermalOutput = zone["minThermalOutput"];
151         thisZoneConfig.failsafePercent = zone["failsafePercent"];
152 
153         auto findTimeInterval = zone.find("cycleIntervalTimeMS");
154         if (findTimeInterval != zone.end())
155         {
156             uint64_t tmp;
157             findTimeInterval->get_to(tmp);
158             if (tmp != 0)
159             {
160                 thisZoneConfig.cycleTime.cycleIntervalTimeMS = tmp;
161             }
162             else
163             {
164                 std::cerr << "cycleIntervalTimeMS cannot be 0. Use default "
165                           << thisZoneConfig.cycleTime.cycleIntervalTimeMS
166                           << " ms\n";
167             }
168         }
169 
170         auto findUpdateThermalsTime = zone.find("updateThermalsTimeMS");
171         if (findUpdateThermalsTime != zone.end())
172         {
173             uint64_t tmp;
174             findUpdateThermalsTime->get_to(tmp);
175             if (tmp != 0)
176             {
177                 thisZoneConfig.cycleTime.updateThermalsTimeMS = tmp;
178             }
179             else
180             {
181                 std::cerr << "updateThermalsTimeMS cannot be 0. Use default "
182                           << thisZoneConfig.cycleTime.updateThermalsTimeMS
183                           << " ms\n";
184             }
185         }
186 
187         double updateCount =
188             double(thisZoneConfig.cycleTime.updateThermalsTimeMS) /
189             double(thisZoneConfig.cycleTime.cycleIntervalTimeMS);
190 
191         /* Check if updateThermalsTimeMS could be divided by cycleIntervalTimeMS
192          * without leaving a remainder */
193         if (updateCount != std::ceil(updateCount))
194         {
195             std::cerr
196                 << "updateThermalsTimeMS cannot be divided by "
197                    "cycleIntervalTimeMS without leaving a remainder. Using the "
198                    "smallest integer that is not less than the result.\n";
199             updateCount = std::ceil(updateCount);
200         }
201         thisZoneConfig.cycleTime.updateThermalsTimeMS = updateCount;
202 
203         auto pids = zone["pids"];
204         for (const auto& pid : pids)
205         {
206             auto name = pid["name"];
207             auto item = pid.get<conf::ControllerInfo>();
208 
209             thisZone[name] = item;
210         }
211 
212         pidConfig[id] = thisZone;
213         zoneConfig[id] = thisZoneConfig;
214     }
215 
216     return std::make_pair(pidConfig, zoneConfig);
217 }
218 
219 } // namespace pid_control
220