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