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 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 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 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 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 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 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 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 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 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 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