blob: 57f1e7e45755d111b959d92a1dedb8aa31821fcb [file] [log] [blame]
Josh Lehande745422020-11-07 02:14:09 -08001/**
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
28namespace pid_control
29{
30namespace ec
31{
32
33// Redundant log entries only once every 60 seconds
34static constexpr int logThrottle = 60 * 1000;
35
36static std::map<std::string, PidCoreLog> nameToLog;
37
38static 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
56static 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
72static 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
85static 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
101static 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
114static 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
133void 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
232PidCoreLog* 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
243void 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
280std::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