xref: /openbmc/phosphor-pid-control/pid/ec/logging.cpp (revision f8b6e55147148c3cfb42327ff267197a460b411c)
1 /**
2  * Copyright 2022 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 "logging.hpp"
18 
19 #include "../tuning.hpp"
20 #include "pid.hpp"
21 
22 #include <chrono>
23 #include <cstddef>
24 #include <fstream>
25 #include <iostream>
26 #include <map>
27 #include <string>
28 #include <utility>
29 
30 namespace pid_control
31 {
32 namespace ec
33 {
34 
35 // Redundant log entries only once every 60 seconds
36 static constexpr int logThrottle = 60 * 1000;
37 
38 static std::map<std::string, PidCoreLog> nameToLog;
39 
CharValid(const std::string::value_type & ch)40 static bool CharValid(const std::string::value_type& ch)
41 {
42     // Intentionally avoiding invoking locale support here
43     if ((ch >= 'A') && (ch <= 'Z'))
44     {
45         return true;
46     }
47     if ((ch >= 'a') && (ch <= 'z'))
48     {
49         return true;
50     }
51     if ((ch >= '0') && (ch <= '9'))
52     {
53         return true;
54     }
55     return false;
56 }
57 
StrClean(const std::string & str)58 static std::string StrClean(const std::string& str)
59 {
60     std::string res;
61     size_t len = str.size();
62     for (size_t i = 0; i < len; ++i)
63     {
64         const auto& c = str[i];
65         if (!(CharValid(c)))
66         {
67             continue;
68         }
69         res += c;
70     }
71     return res;
72 }
73 
DumpContextHeader(std::ofstream & file)74 static void DumpContextHeader(std::ofstream& file)
75 {
76     file << "epoch_ms,input,setpoint,error";
77     file << ",proportionalTerm";
78     file << ",integralTerm1,integralTerm2";
79     file << ",derivativeTerm";
80     file << ",feedFwdTerm,output1,output2";
81     file << ",minOut,maxOut";
82     file << ",integralTerm3,output3";
83     file << ",integralTerm,output";
84     file << "\n" << std::flush;
85 }
86 
DumpContextData(std::ofstream & file,const std::chrono::milliseconds & msNow,const PidCoreContext & pc)87 static void DumpContextData(std::ofstream& file,
88                             const std::chrono::milliseconds& msNow,
89                             const PidCoreContext& pc)
90 {
91     file << msNow.count();
92     file << "," << pc.input << "," << pc.setpoint << "," << pc.error;
93     file << "," << pc.proportionalTerm;
94     file << "," << pc.integralTerm1 << "," << pc.integralTerm2;
95     file << "," << pc.derivativeTerm;
96     file << "," << pc.feedFwdTerm << "," << pc.output1 << "," << pc.output2;
97     file << "," << pc.minOut << "," << pc.maxOut;
98     file << "," << pc.integralTerm3 << "," << pc.output3;
99     file << "," << pc.integralTerm << "," << pc.output;
100     file << "\n" << std::flush;
101 }
102 
DumpCoeffsHeader(std::ofstream & file)103 static void DumpCoeffsHeader(std::ofstream& file)
104 {
105     file << "epoch_ms,ts,integral,lastOutput";
106     file << ",proportionalCoeff,integralCoeff";
107     file << ",derivativeCoeff";
108     file << ",feedFwdOffset,feedFwdGain";
109     file << ",integralLimit.min,integralLimit.max";
110     file << ",outLim.min,outLim.max";
111     file << ",slewNeg,slewPos";
112     file << ",positiveHysteresis,negativeHysteresis";
113     file << "\n" << std::flush;
114 }
115 
DumpCoeffsData(std::ofstream & file,const std::chrono::milliseconds & msNow,pid_info_t * pidinfoptr)116 static void DumpCoeffsData(std::ofstream& file,
117                            const std::chrono::milliseconds& msNow,
118                            pid_info_t* pidinfoptr)
119 {
120     // Save some typing
121     const auto& p = *pidinfoptr;
122 
123     file << msNow.count();
124     file << "," << p.ts << "," << p.integral << "," << p.lastOutput;
125     file << "," << p.proportionalCoeff << "," << p.integralCoeff;
126     file << "," << p.derivativeCoeff;
127     file << "," << p.feedFwdOffset << "," << p.feedFwdGain;
128     file << "," << p.integralLimit.min << "," << p.integralLimit.max;
129     file << "," << p.outLim.min << "," << p.outLim.max;
130     file << "," << p.slewNeg << "," << p.slewPos;
131     file << "," << p.positiveHysteresis << "," << p.negativeHysteresis;
132     file << "\n" << std::flush;
133 }
134 
LogInit(const std::string & name,pid_info_t * pidinfoptr)135 void LogInit(const std::string& name, pid_info_t* pidinfoptr)
136 {
137     if (!coreLoggingEnabled)
138     {
139         // PID logging not enabled by configuration, silently do nothing
140         return;
141     }
142 
143     if (name.empty())
144     {
145         std::cerr << "PID logging disabled because PID does not have a name\n";
146         return;
147     }
148 
149     std::string cleanName = StrClean(name);
150     if (cleanName.empty())
151     {
152         std::cerr << "PID logging disabled because PID name is unusable: "
153                   << name << "\n";
154         return;
155     }
156 
157     auto iterExisting = nameToLog.find(name);
158 
159     if (iterExisting != nameToLog.end())
160     {
161         std::cerr << "PID logging reusing existing file: " << name << "\n";
162     }
163     else
164     {
165         // Multiple names could collide to the same clean name
166         // Make sure clean name is not already used
167         for (const auto& iter : nameToLog)
168         {
169             if (iter.second.nameClean == cleanName)
170             {
171                 std::cerr << "PID logging disabled because of name collision: "
172                           << name << "\n";
173                 return;
174             }
175         }
176 
177         std::string filec = loggingPath + "/pidcore." + cleanName;
178         std::string filef = loggingPath + "/pidcoeffs." + cleanName;
179 
180         std::ofstream outc;
181         std::ofstream outf;
182 
183         outc.open(filec);
184         if (!(outc.good()))
185         {
186             std::cerr << "PID logging disabled because unable to open file: "
187                       << filec << "\n";
188             return;
189         }
190 
191         outf.open(filef);
192         if (!(outf.good()))
193         {
194             // Be sure to clean up all previous initialization
195             outf.close();
196 
197             std::cerr << "PID logging disabled because unable to open file: "
198                       << filef << "\n";
199             return;
200         }
201 
202         PidCoreLog newLog;
203 
204         // All good, commit to doing logging by moving into the map
205         newLog.nameOriginal = name;
206         newLog.nameClean = cleanName;
207         newLog.fileContext = std::move(outc);
208         newLog.fileCoeffs = std::move(outf);
209 
210         // The streams within this object are not copyable, must move them
211         nameToLog[name] = std::move(newLog);
212 
213         // This must now succeed, as it must be in the map
214         iterExisting = nameToLog.find(name);
215 
216         // Write headers only when creating files for the first time
217         DumpContextHeader(iterExisting->second.fileContext);
218         DumpCoeffsHeader(iterExisting->second.fileCoeffs);
219 
220         std::cerr << "PID logging initialized: " << name << "\n";
221     }
222 
223     auto msNow = LogTimestamp();
224 
225     // Write the coefficients only once per PID loop initialization
226     // If they change, caller will reinitialize the PID loops
227     DumpCoeffsData(iterExisting->second.fileCoeffs, msNow, pidinfoptr);
228 
229     // Force the next logging line to be logged
230     iterExisting->second.lastLog = iterExisting->second.lastLog.zero();
231     iterExisting->second.lastContext = PidCoreContext();
232 }
233 
LogPeek(const std::string & name)234 PidCoreLog* LogPeek(const std::string& name)
235 {
236     auto iter = nameToLog.find(name);
237     if (iter != nameToLog.end())
238     {
239         return &(iter->second);
240     }
241 
242     return nullptr;
243 }
244 
LogContext(PidCoreLog & pidLog,const std::chrono::milliseconds & msNow,const PidCoreContext & coreContext)245 void LogContext(PidCoreLog& pidLog, const std::chrono::milliseconds& msNow,
246                 const PidCoreContext& coreContext)
247 {
248     bool shouldLog = false;
249 
250     if (pidLog.lastLog == pidLog.lastLog.zero())
251     {
252         // It is the first time
253         shouldLog = true;
254     }
255     else
256     {
257         auto since = msNow - pidLog.lastLog;
258         if (since.count() >= logThrottle)
259         {
260             // It has been long enough since the last time
261             shouldLog = true;
262         }
263     }
264 
265     if (pidLog.lastContext != coreContext)
266     {
267         // The content is different
268         shouldLog = true;
269     }
270 
271     if (!shouldLog)
272     {
273         return;
274     }
275 
276     pidLog.lastLog = msNow;
277     pidLog.lastContext = coreContext;
278 
279     DumpContextData(pidLog.fileContext, msNow, coreContext);
280 }
281 
LogTimestamp(void)282 std::chrono::milliseconds LogTimestamp(void)
283 {
284     auto clockNow = std::chrono::high_resolution_clock::now();
285     auto msNow = std::chrono::duration_cast<std::chrono::milliseconds>(
286         clockNow.time_since_epoch());
287     return msNow;
288 }
289 
290 } // namespace ec
291 } // namespace pid_control
292