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