17#if __cplusplus >= 201703L
19namespace fs = std::filesystem;
27 return (stat(
path.c_str(), &buffer) == 0);
31 if (stat(
path.c_str(), &buffer) != 0)
33 return S_ISDIR(buffer.st_mode);
37 path(
const std::string& s) :
p(s) {
43 size_t pos =
p.find_last_of(
'.');
44 return (pos != std::string::npos) ?
p.substr(pos) :
"";
51 dir = opendir(dir_path.c_str());
62 return entry !=
nullptr;
65 return entry ? std::string(
entry->d_name) :
"";
71 std::string full_path = std::string(
dir->d_name) +
"/" +
entry->d_name;
72 if (stat(full_path.c_str(), &buffer) != 0)
74 return S_ISREG(buffer.st_mode);
86 "external/DiabloAvionics/PT_Board/Calibration/PT Calibration Attempt 2026-02-04_test2.csv";
99 const char* jf = std::getenv(
"CAL_PT_JSON_FIRST");
100 const bool json_first =
101 jf && (jf[0] ==
'1' || jf[0] ==
'y' || jf[0] ==
'Y' || jf[0] ==
't' || jf[0] ==
'T');
103 auto try_csv = [
this]() ->
bool {
106 std::cout <<
"[PTCalibration] Loaded " <<
calibrations_.size()
114 auto try_json = [
this]() ->
bool {
118 std::cout <<
"[PTCalibration] Loaded " <<
calibrations_.size()
119 <<
" calibrations from JSON: " << json_file << std::endl;
136 const char* missing_only_env = std::getenv(
"CAL_PT_JSON_MISSING_ONLY");
137 const bool json_missing_only =
138 missing_only_env && (missing_only_env[0] ==
'1' || missing_only_env[0] ==
'y' ||
139 missing_only_env[0] ==
'Y' || missing_only_env[0] ==
't' ||
140 missing_only_env[0] ==
'T');
143 if (!json_file.empty() &&
load_from_json(json_file, json_missing_only)) {
144 if (json_missing_only) {
145 std::cout <<
"[PTCalibration] Merged missing channels from JSON only: "
147 <<
" logical channels)" << std::endl;
149 std::cout <<
"[PTCalibration] Overlaid GUI JSON on CSV: " << json_file
150 <<
" (logical channels present in JSON replace CSV; total keys "
161 std::cout <<
"[PTCalibration] ⚠️ No calibration files found - sensors will be uncalibrated"
167 std::ifstream file(json_path);
168 if (!file.is_open()) {
169 std::cerr <<
"[PTCalibration] Failed to open JSON file: " << json_path << std::endl;
174 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
179 root = nlohmann::json::parse(content);
180 }
catch (
const std::exception& e) {
181 std::cerr <<
"[PTCalibration] JSON parse error in " << json_path <<
": " << e.what()
185 if (!root.contains(
"calibration_polynomials") || !root[
"calibration_polynomials"].is_object())
189 for (
const auto& [ch_str, arr] : root[
"calibration_polynomials"].items()) {
190 if (!arr.is_array() || arr.size() < 4)
197 ch = std::stoi(ch_str);
201 const uint8_t channel_id =
static_cast<uint8_t
>(
ch);
205 arr[2].get<double>(), arr[3].get<double>());
213 std::ifstream file(csv_path);
214 if (!file.is_open()) {
215 std::cerr <<
"[PTCalibration] Failed to open CSV file: " << csv_path << std::endl;
220 std::string header_line;
221 std::getline(file, header_line);
224 std::regex pt_col_regex(R
"(PT(\d+)\s+Coefficient\s+(\d+))", std::regex_constants::icase);
225 std::sregex_iterator iter(header_line.begin(), header_line.end(), pt_col_regex);
226 std::sregex_iterator end;
228 std::map<uint8_t, std::array<int, 4>> pt_col_indices;
229 std::vector<std::string> columns;
232 std::istringstream header_stream(header_line);
235 while (std::getline(header_stream, col,
',')) {
236 columns.push_back(col);
238 if (std::regex_search(col, match, pt_col_regex)) {
239 uint8_t pt_num =
static_cast<uint8_t
>(std::stoi(match[1].str()));
240 int coeff_idx = std::stoi(match[2].str());
241 if (coeff_idx >= 0 && coeff_idx < 4) {
242 pt_col_indices[pt_num][coeff_idx] = col_idx;
249 std::vector<std::vector<std::string>> rows;
251 while (std::getline(file, line)) {
254 std::vector<std::string> row;
255 std::istringstream line_stream(line);
257 while (std::getline(line_stream, cell,
',')) {
268 const auto& last_row = rows.back();
271 for (
const auto& [pt_num, col_indices] : pt_col_indices) {
272 if (col_indices[0] <
static_cast<int>(last_row.size()) &&
273 col_indices[1] <
static_cast<int>(last_row.size()) &&
274 col_indices[2] <
static_cast<int>(last_row.size()) &&
275 col_indices[3] <
static_cast<int>(last_row.size())) {
277 double A = std::stod(last_row[col_indices[0]]);
278 double B = std::stod(last_row[col_indices[1]]);
279 double C = std::stod(last_row[col_indices[2]]);
280 double D = std::stod(last_row[col_indices[3]]);
283 }
catch (
const std::exception& e) {
284 std::cerr <<
"[PTCalibration] Error parsing coefficients for PT "
285 <<
static_cast<int>(pt_num) <<
": " << e.what() << std::endl;
316 double p = coeffs->calculate_pressure(adc_code);
317 if (!std::isfinite(
p))
319 return std::clamp(
p, -3000.0, 20000.0);
325 auto eval = [
this](
double x) {
326 return (
A * x * x * x) + (
B * x * x) + (
C * x) +
D;
328 for (
const auto& [lo, hi] : {std::pair<int64_t, int64_t>(0, 2147483647),
329 std::pair<int64_t, int64_t>(-2147483648, 0)}) {
330 double f_lo = eval(lo);
331 double f_hi = eval(hi);
332 if (!(std::min(f_lo, f_hi) <= target_psi && target_psi <= std::max(f_lo, f_hi)))
334 double left = lo, right = hi;
335 for (
int i = 0; i < 64; ++i) {
336 double mid = std::round((left + right) / 2);
337 double f_mid = eval(mid);
338 if (std::abs(f_mid - target_psi) < 0.5)
339 return static_cast<int32_t
>(mid);
341 if (f_mid < target_psi)
346 if (f_mid > target_psi)
352 return static_cast<int32_t
>(std::round((left + right) / 2));
358 const std::string& csv_path) {
366 if (filename ==
"adjustments.json")
368 if (filename.find(
"learned_prior") != std::string::npos)
373 if (filename ==
"cubic_calibration.json")
383 std::string latest_file;
384 std::time_t latest_time = 0;
391 bool have_candidate =
false;
393#if __cplusplus >= 201703L
395 if (!entry.is_regular_file() || entry.path().extension() !=
".json")
397 const std::string fname = entry.path().filename().string();
400 auto file_time = fs::last_write_time(entry.path());
402 std::chrono::duration_cast<std::chrono::seconds>(file_time.time_since_epoch()).count();
404 if (!have_candidate || time_t > latest_time) {
405 have_candidate =
true;
406 latest_time = time_t;
407 latest_file = entry.path().string();
412 DIR* dir = opendir(json_dir.c_str());
414 struct dirent* entry;
415 while ((entry = readdir(dir)) !=
nullptr) {
416 std::string filename = entry->d_name;
417 if (filename.length() > 5 && filename.substr(filename.length() - 5) ==
".json") {
420 std::string full_path = json_dir +
"/" + filename;
421 struct stat file_stat;
422 if (stat(full_path.c_str(), &file_stat) == 0 && S_ISREG(file_stat.st_mode)) {
423 if (!have_candidate || file_stat.st_mtime > latest_time) {
424 have_candidate =
true;
425 latest_time = file_stat.st_mtime;
426 latest_file = full_path;
std::string find_latest_json_file(const std::string &json_dir) const
PTCalibrationManager(bool auto_load=true)
bool load_calibration()
Auto-load calibration from default paths Default: factory CSV first, then newest GUI JSON overlays ev...
bool load_from_json(const std::string &json_path, bool merge_missing_only=false)
Load calibration from JSON file (from calibration GUI).
void set_calibration(uint8_t channel_id, const PTCalibrationCoeffs &coeffs)
Set/replace the cubic coefficients for a logical channel at runtime.
std::map< uint8_t, PTCalibrationCoeffs > calibrations_
bool is_calibrated(uint8_t channel_id) const
Check if a channel has calibration.
static std::string default_json_dir_
static std::string default_csv_path_
double calculate_pressure(uint8_t channel_id, int32_t adc_code) const
Calculate calibrated pressure for a channel.
void clear_calibration(uint8_t channel_id)
Remove any cubic for a logical channel, so is_calibrated() returns false again.
bool load_from_csv(const std::string &csv_path)
Load calibration from CSV file (from DiabloAvionics).
const PTCalibrationCoeffs * get_calibration(uint8_t channel_id) const
Get calibration coefficients for a sensor channel.
static void set_default_paths(const std::string &json_dir, const std::string &csv_path)
Set default calibration file paths.
*brief create JSON pointer *sa reference_tokens end()
bool is_directory(const std::string &path)
bool exists(const std::string &path)
Output smoothing for calibrated PT pressure, per sensor uid (ESP32 and LabJack T7 alike).
static bool skip_json_for_pt_polynomial_load(const std::string &filename)
Match calibration_orchestrator._load_prior_from_polynomial_calibration: skip non-factory JSON.
Motorized ball valve position: raw ADC counts -> % open.
bool is_regular_file() const
directory_iterator(const std::string &dir_path)
std::string string() const
std::string extension() const
path(const std::string &s)
PT Calibration Coefficients.
std::optional< int32_t > invert_to_adc(double target_psi) const
Invert psi = A*x^3 + B*x^2 + C*x + D to find ADC code for target PSI.
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")