STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
SensorCalibration.cpp
Go to the documentation of this file.
2
3#include <dirent.h>
4#include <sys/stat.h>
5
6#include <algorithm>
7#include <cctype>
8#include <filesystem>
9#include <fstream>
10#include <iostream>
11#include <nlohmann/json.hpp>
12#include <regex>
13#include <sstream>
14
15namespace fs = std::filesystem;
16
17namespace fsw {
18namespace calibration {
19
20// ── Loading ────────────────────────────────────────────────────────────────
21
22bool SensorCalibrationManager::load_from_json(const std::string& json_path) {
23 std::ifstream file(json_path);
24 if (!file.is_open()) {
25 std::cerr << "[" << sensor_type_ << " Cal] Failed to open JSON: " << json_path << std::endl;
26 return false;
27 }
28
29 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
30 file.close();
31
32 nlohmann::json root;
33 try {
34 root = nlohmann::json::parse(content);
35 } catch (const std::exception& e) {
36 std::cerr << "[" << sensor_type_ << " Cal] JSON parse error in " << json_path << ": "
37 << e.what() << std::endl;
38 return false;
39 }
40 // Coefficients live under "calibration_polynomials" when present, else at the top level
41 // ({"1": [c0, c1, ...], ...}).
42 //
43 // A load cell reads its OWN namespace first. PT and LC share a logical-channel space
44 // (slot 2 connector 1 is channel 11 for both a PT board 22 and an LC board 42), so a
45 // single map let one overwrite the other — a load cell's curve landed on a 5000 psi
46 // transducer's channel on 2026-09-13. The lc_ map is written separately now; the fallback
47 // chain keeps files written before that split readable.
48 const std::string own_key =
49 sensor_type_ == "LC" ? "lc_calibration_polynomials" : "calibration_polynomials";
50 const nlohmann::json& poly =
51 (root.contains(own_key) && root[own_key].is_object() && !root[own_key].empty())
52 ? root[own_key]
53 : (root.contains("calibration_polynomials") && root["calibration_polynomials"].is_object())
54 ? root["calibration_polynomials"]
55 : root;
56 if (!poly.is_object())
57 return false;
58
59 auto is_numeric = [](const std::string& s) {
60 return !s.empty() && std::all_of(s.begin(), s.end(), [](unsigned char c) {
61 return std::isdigit(c) != 0;
62 });
63 };
64
65 size_t loaded = 0;
66 for (const auto& [ch_str, arr] : poly.items()) {
67 if (!is_numeric(ch_str) || !arr.is_array() || arr.empty())
68 continue;
69 std::vector<double> coeffs;
70 bool ok = true;
71 for (const auto& e : arr) {
72 if (!e.is_number()) {
73 ok = false;
74 break;
75 }
76 coeffs.push_back(e.get<double>());
77 }
78 if (!ok || coeffs.empty())
79 continue;
80 const uint8_t channel_id = static_cast<uint8_t>(std::stoi(ch_str));
82 loaded++;
83 }
84
85 if (loaded > 0) {
86 std::cout << "[" << sensor_type_ << " Cal] Loaded " << loaded
87 << " calibrations from JSON: " << json_path << std::endl;
88 }
89 return loaded > 0;
90}
91
92bool SensorCalibrationManager::load_from_csv(const std::string& csv_path) {
93 std::ifstream file(csv_path);
94 if (!file.is_open()) {
95 std::cerr << "[" << sensor_type_ << " Cal] Failed to open CSV: " << csv_path << std::endl;
96 return false;
97 }
98
99 // Read header line
100 std::string header_line;
101 std::getline(file, header_line);
102
103 // Find columns: "{TYPE}{N} Coefficient {K}"
104 // e.g. "PT1 Coefficient 0", "TC2 Coefficient 1", etc.
105 std::string pattern = sensor_type_ + "(\\d+)\\s+Coefficient\\s+(\\d+)";
106 std::regex col_regex(pattern, std::regex_constants::icase);
107
108 // Parse header → column indices per channel
109 std::map<uint8_t, std::map<int, int>> channel_coeff_cols; // channel → (coeff_idx → col_idx)
110 {
111 std::istringstream ss(header_line);
112 std::string col;
113 int col_idx = 0;
114 while (std::getline(ss, col, ',')) {
115 std::smatch m;
116 if (std::regex_search(col, m, col_regex)) {
117 uint8_t ch = static_cast<uint8_t>(std::stoi(m[1].str()));
118 int coeff_idx = std::stoi(m[2].str());
119 channel_coeff_cols[ch][coeff_idx] = col_idx;
120 }
121 col_idx++;
122 }
123 }
124
125 // Read all rows, use last row for coefficients
126 std::vector<std::vector<std::string>> rows;
127 std::string line;
128 while (std::getline(file, line)) {
129 if (line.empty())
130 continue;
131 std::vector<std::string> row;
132 std::istringstream ss(line);
133 std::string cell;
134 while (std::getline(ss, cell, ','))
135 row.push_back(cell);
136 rows.push_back(row);
137 }
138
139 if (rows.empty())
140 return false;
141 const auto& last_row = rows.back();
142
143 for (const auto& [ch, coeff_map] : channel_coeff_cols) {
144 std::vector<double> coeffs(coeff_map.size(), 0.0);
145 bool ok = true;
146 for (const auto& [ci, col] : coeff_map) {
147 if (col >= static_cast<int>(last_row.size())) {
148 ok = false;
149 break;
150 }
151 try {
152 if (ci < static_cast<int>(coeffs.size()))
153 coeffs[ci] = std::stod(last_row[col]);
154 } catch (...) {
155 ok = false;
156 break;
157 }
158 }
159 if (ok && !coeffs.empty()) {
161 }
162 }
163
164 if (!calibrations_.empty()) {
165 std::cout << "[" << sensor_type_ << " Cal] Loaded " << calibrations_.size()
166 << " calibrations from CSV: " << csv_path << std::endl;
167 }
168 return !calibrations_.empty();
169}
170
171bool SensorCalibrationManager::load_calibration(const std::string& json_dir,
172 const std::string& csv_path) {
173 // JSON first
174 if (!json_dir.empty() && fs::exists(json_dir)) {
175 std::string json_file = find_latest_json(json_dir);
176 if (!json_file.empty() && load_from_json(json_file))
177 return true;
178 }
179 // CSV fallback
180 if (!csv_path.empty() && fs::exists(csv_path)) {
181 if (load_from_csv(csv_path))
182 return true;
183 }
184 std::cout << "[" << sensor_type_ << " Cal] ⚠️ No calibration files found — sensors uncalibrated"
185 << std::endl;
186 return false;
187}
188
189// ── Evaluation ─────────────────────────────────────────────────────────────
190
191bool SensorCalibrationManager::is_calibrated(uint8_t channel_id) const {
192 return calibrations_.count(channel_id) > 0;
193}
194
195double SensorCalibrationManager::calculate(uint8_t channel_id, int32_t raw_counts) const {
196 auto it = calibrations_.find(channel_id);
197 if (it != calibrations_.end()) {
198 return it->second.evaluate(raw_counts);
199 }
200 return 0.0;
201}
202
203const PolynomialCalibration* SensorCalibrationManager::get(uint8_t channel_id) const {
204 auto it = calibrations_.find(channel_id);
205 return (it != calibrations_.end()) ? &it->second : nullptr;
206}
207
208// ── Helpers ────────────────────────────────────────────────────────────────
209
210std::string SensorCalibrationManager::find_latest_json(const std::string& dir) const {
211 if (!fs::exists(dir) || !fs::is_directory(dir))
212 return "";
213
214 std::string latest;
215 std::filesystem::file_time_type latest_time{};
216
217 for (const auto& entry : fs::directory_iterator(dir)) {
218 if (entry.is_regular_file() && entry.path().extension() == ".json") {
219 auto ftime = fs::last_write_time(entry.path());
220 if (latest.empty() || ftime > latest_time) {
221 latest_time = ftime;
222 latest = entry.path().string();
223 }
224 }
225 }
226 return latest;
227}
228
229} // namespace calibration
230} // namespace fsw
double calculate(uint8_t channel_id, int32_t raw_counts) const
const PolynomialCalibration * get(uint8_t channel_id) const
bool load_calibration(const std::string &json_dir, const std::string &csv_path)
Auto-discover & load from default directories.
bool load_from_csv(const std::string &csv_path)
Load calibration from a CSV file (DiabloAvionics format).
std::string find_latest_json(const std::string &dir) const
bool is_calibrated(uint8_t channel_id) const
bool load_from_json(const std::string &json_path)
Load calibration from a JSON calibration file.
std::map< uint8_t, PolynomialCalibration > calibrations_
static bool ok
Definition flight_e2e.c:435
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).
Motorized ball valve position: raw ADC counts -> % open.
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")
Definition test_t7.cpp:98