STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
LcTareStore.cpp
Go to the documentation of this file.
2
3#include <chrono>
4#include <cmath>
5#include <cstring>
6#include <filesystem>
7#include <fstream>
8#include <iostream>
9#include <iterator>
10#include <nlohmann/json.hpp>
11
12namespace fsw {
13namespace calibration {
14
15namespace {
16
17double unix_now_ms() {
18 return std::chrono::duration<double, std::milli>(
19 std::chrono::system_clock::now().time_since_epoch())
20 .count();
21}
22
23/** Probe codes spanning the signed 32-bit ADC range the LC boards report. Fixed forever: they
24 * only have to be the SAME codes across two fingerprints, never meaningful loads. */
25constexpr double kProbes[] = {-1.0e9, -1.0e6, 0.0, 1.0e6, 1.0e9};
26
27} // namespace
28
29std::string lc_tare_entity(uint8_t board_id, uint8_t connector) {
30 const int m = static_cast<int>(board_id) % 10;
31 const int slot = (m == 0 ? 10 : m);
32 return "LC" + std::to_string(slot) + "_Cal.CH" + std::to_string(static_cast<int>(connector));
33}
34
35LcTareStore::LcTareStore(std::string file_path) : file_path_(std::move(file_path)) {
36}
37
38uint64_t LcTareStore::fingerprint(const Evaluator& eval) {
39 if (!eval)
40 return 0;
41 // FNV-1a over the raw bits of each probe's result. A non-finite result is folded in as a
42 // fixed sentinel so a blown-up curve still fingerprints deterministically rather than
43 // hashing whichever NaN payload the FPU produced.
44 uint64_t h = 1469598103934665603ull;
45 for (double probe : kProbes) {
46 const double v = eval(probe);
47 uint64_t bits;
48 if (std::isfinite(v))
49 std::memcpy(&bits, &v, sizeof(bits));
50 else
51 bits = 0xDEADBEEFDEADBEEFull;
52 for (size_t i = 0; i < sizeof(bits); ++i) {
53 h ^= static_cast<uint64_t>((bits >> (i * 8)) & 0xFF);
54 h *= 1099511628211ull;
55 }
56 }
57 return h;
58}
59
61 if (!eval)
62 return false;
63 const double kg = eval(t.adc_at_tare);
64 if (!std::isfinite(kg))
65 return false;
66 t.offset_kg = kg;
67 t.curve_fp = fingerprint(eval);
68 return true;
69}
70
71bool LcTareStore::set(uint16_t uid, const std::string& entity, double adc_at_tare,
72 const Evaluator& eval) {
73 std::lock_guard<std::mutex> lock(mutex_);
74 if (!std::isfinite(adc_at_tare))
75 return false;
76
77 LcTare t;
78 t.uid = uid;
79 t.entity = entity;
80 t.adc_at_tare = adc_at_tare;
81 t.set_at_ms = unix_now_ms();
82 if (!recompute_locked(t, eval)) {
83 // Nothing is recorded. A tare whose offset cannot be evaluated is worse than no tare:
84 // Node would subtract it from every sample on the channel.
85 std::cout << "[LcTare] uid " << static_cast<int>(uid)
86 << ": curve yields no finite offset at adc " << adc_at_tare
87 << " — tare not recorded" << std::endl;
88 return false;
89 }
90 tares_[uid] = t;
91 return true;
92}
93
94void LcTareStore::clear(uint16_t uid) {
95 std::lock_guard<std::mutex> lock(mutex_);
96 tares_.erase(uid);
97}
98
100 std::lock_guard<std::mutex> lock(mutex_);
101 tares_.clear();
102}
103
104void LcTareStore::recompute(uint16_t uid, const Evaluator& eval) {
105 std::lock_guard<std::mutex> lock(mutex_);
106 if (!curves_trusted_)
107 return;
108 auto it = tares_.find(uid);
109 if (it == tares_.end())
110 return;
111 LcTare probe = it->second;
112 if (recompute_locked(probe, eval))
113 it->second = probe;
114 // else: keep the last good offset. Writing through a curve that evaluates to NaN would
115 // replace a usable number with one that kills the series downstream.
116}
117
118void LcTareStore::recompute_all(const std::function<Evaluator(uint16_t)>& eval_for) {
119 std::lock_guard<std::mutex> lock(mutex_);
120 if (!curves_trusted_)
121 return;
122 if (!eval_for)
123 return;
124 for (auto& [uid, t] : tares_) {
125 LcTare probe = t;
126 if (recompute_locked(probe, eval_for(uid)))
127 t = probe;
128 }
129}
130
131size_t LcTareStore::recompute_stale(const std::function<Evaluator(uint16_t)>& eval_for) {
132 std::lock_guard<std::mutex> lock(mutex_);
133 if (!curves_trusted_ || !eval_for)
134 return 0;
135 size_t stale = 0;
136 for (auto& [uid, t] : tares_) {
137 const Evaluator eval = eval_for(uid);
138 if (fingerprint(eval) == t.curve_fp)
139 continue;
140 ++stale;
141 LcTare probe = t;
142 if (recompute_locked(probe, eval))
143 t = probe;
144 }
145 return stale;
146}
147
149 std::lock_guard<std::mutex> lock(mutex_);
150 curves_trusted_ = trusted;
151}
152
154 std::lock_guard<std::mutex> lock(mutex_);
155 return curves_trusted_;
156}
157
158const LcTare* LcTareStore::tare_for(uint16_t uid) const {
159 std::lock_guard<std::mutex> lock(mutex_);
160 auto it = tares_.find(uid);
161 return it == tares_.end() ? nullptr : &it->second;
162}
163
164std::vector<uint16_t> LcTareStore::uids() const {
165 std::lock_guard<std::mutex> lock(mutex_);
166 std::vector<uint16_t> out;
167 out.reserve(tares_.size());
168 for (const auto& [uid, t] : tares_)
169 out.push_back(uid);
170 return out;
171}
172
173size_t LcTareStore::size() const {
174 std::lock_guard<std::mutex> lock(mutex_);
175 return tares_.size();
176}
177
179 std::lock_guard<std::mutex> lock(mutex_);
180 return load_failed_;
181}
182
183std::string LcTareStore::serialize() const {
184 nlohmann::json root;
185 root["version"] = 1;
186 nlohmann::json arr = nlohmann::json::array();
187 for (const auto& [uid, t] : tares_) {
188 nlohmann::json tj;
189 tj["uid"] = t.uid;
190 tj["entity"] = t.entity;
191 tj["adc_at_tare"] = t.adc_at_tare;
192 tj["offset_kg"] = t.offset_kg;
193 tj["set_at_ms"] = t.set_at_ms;
194 tj["curve_fp"] = t.curve_fp;
195 arr.push_back(tj);
196 }
197 root["tares"] = arr;
198 return root.dump(2);
199}
200
201bool LcTareStore::save() const {
202 std::lock_guard<std::mutex> lock(mutex_);
203 if (load_failed_) {
204 // Same rule as CubicCalibrationStore: a file we could not read is a file we must not
205 // replace, because this store is empty precisely because the read failed.
206 return false;
207 }
208 try {
209 std::filesystem::path p(file_path_);
210 if (p.has_parent_path())
211 std::filesystem::create_directories(p.parent_path());
212 const std::string tmp = file_path_ + ".tmp";
213 {
214 std::ofstream f(tmp, std::ios::trunc);
215 if (!f.is_open())
216 return false;
217 f << serialize();
218 f.flush();
219 if (!f.good())
220 return false;
221 }
222 std::filesystem::rename(tmp, file_path_); // atomic replace on same filesystem
223 return true;
224 } catch (...) {
225 return false;
226 }
227}
228
230 std::lock_guard<std::mutex> lock(mutex_);
231 tares_.clear();
232
233 std::ifstream f(file_path_);
234 if (!f.is_open()) {
235 // No file is the normal state: the backend unlinks it at session start, and every
236 // session begins with every load cell reading absolute. Not a failure.
237 load_failed_ = false;
238 return 0;
239 }
240 std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
241
242 nlohmann::json root;
243 try {
244 root = nlohmann::json::parse(content);
245 } catch (...) {
246 load_failed_ = true;
247 std::cout << "[LcTare] " << file_path_ << " could not be parsed — refusing to overwrite it"
248 << std::endl;
249 return 0;
250 }
251 if (!root.contains("tares") || !root["tares"].is_array()) {
252 load_failed_ = true;
253 std::cout << "[LcTare] " << file_path_ << " has no tares array — refusing to overwrite it"
254 << std::endl;
255 return 0;
256 }
257 load_failed_ = false;
258
259 size_t loaded = 0;
260 for (const auto& tj : root["tares"]) {
261 if (!tj.is_object())
262 continue;
263 LcTare t;
264 t.uid = static_cast<uint16_t>(tj.value("uid", 0));
265 if (t.uid == 0)
266 continue;
267 t.entity = tj.value("entity", std::string());
268 t.adc_at_tare = tj.value("adc_at_tare", 0.0);
269 t.offset_kg = tj.value("offset_kg", 0.0);
270 t.set_at_ms = tj.value("set_at_ms", 0.0);
271 t.curve_fp = tj.value("curve_fp", static_cast<uint64_t>(0));
272 if (!std::isfinite(t.adc_at_tare) || !std::isfinite(t.offset_kg))
273 continue;
274 tares_[t.uid] = t;
275 ++loaded;
276 }
277 return loaded;
278}
279
280} // namespace calibration
281} // namespace fsw
* array(stored with pointer to save storage) array_t *array
bool load_failed() const
Set when load() could not read an existing file; blocks save() so an unreadable store is never replac...
bool recompute_locked(LcTare &t, const Evaluator &eval)
Caller holds mutex_.
bool set(uint16_t uid, const std::string &entity, double adc_at_tare, const Evaluator &eval)
Record a tare for uid at adc_at_tare and derive its offset.
size_t load()
Load the JSON record.
const LcTare * tare_for(uint16_t uid) const
void recompute(uint16_t uid, const Evaluator &eval)
Re-derive offset_kg for one uid under the current curve.
void set_curves_trusted(bool trusted)
False while the curves cannot be trusted — set when the cubic store failed to load.
std::map< uint16_t, LcTare > tares_
std::vector< uint16_t > uids() const
size_t recompute_stale(const std::function< Evaluator(uint16_t)> &eval_for)
Re-derive only the tares whose recorded fingerprint disagrees with the curve now live,...
LcTareStore(std::string file_path)
bool save() const
Atomically write the JSON record (temp + rename).
void recompute_all(const std::function< Evaluator(uint16_t)> &eval_for)
Re-derive every tare; eval_for supplies each uid's evaluator.
std::function< double(double adc)> Evaluator
adc -> kg through whichever model this uid streams (select_lc_kg: cubic fit or the datasheet physics ...
static uint64_t fingerprint(const Evaluator &eval)
A fingerprint of what a curve does, not of the coefficients that describe it: the evaluator is sample...
std::string serialize() const
static emmc_status_t probe(emmc_log_t *log, uint32_t k, bool *valid, uint32_t *session)
Definition emmc_log.c:284
static uint8_t board_id
Definition main.cpp:30
int32_t p
Definition flight_e2e.c:78
*brief remove last reference token *sa JSON pointer has no nullptr
Definition json.hpp:13968
static osMutexId_t lock
Definition mode.c:11
constexpr double kProbes[]
Probe codes spanning the signed 32-bit ADC range the LC boards report.
Output smoothing for calibrated PT pressure, per sensor uid (ESP32 and LabJack T7 alike).
std::string lc_tare_entity(uint8_t board_id, uint8_t connector)
The publish-path entity name for an LC channel: "LC<slot>_Cal.CH<connector>", where slot is board_id ...
Motorized ball valve position: raw ADC counts -> % open.
STL namespace.
One load cell's tare: the scale reads zero at the load it was holding when the operator pressed Tare.