blob: 0b8e7c9997b61ada3f7fffaf8bfa0f197206fd40 [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>
Ed Tanousf8b6e552025-06-27 13:27:50 -070023#include <cstddef>
Josh Lehande745422020-11-07 02:14:09 -080024#include <fstream>
25#include <iostream>
26#include <map>
27#include <string>
Ed Tanousf8b6e552025-06-27 13:27:50 -070028#include <utility>
Josh Lehande745422020-11-07 02:14:09 -080029
30namespace pid_control
31{
32namespace ec
33{
34
35// Redundant log entries only once every 60 seconds
36static constexpr int logThrottle = 60 * 1000;
37
38static std::map<std::string, PidCoreLog> nameToLog;
39
40static 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
58static 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
74static 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
87static 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
103static 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
116static 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
135void 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
234PidCoreLog* 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
245void 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
282std::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