added math signals

This commit is contained in:
Martino Ferrari committed 2026-10-03 17:28:17 +02:00
1 parent 02620ca946
commit aa2508e431
10 files changed
+1773 -27

No files matched your search

+154 -7
View File
@@ -37,13 +37,15 @@ std::string config_dir() {
}
void mkdirs(const std::string& path) {
// **Every** component, the last one included. The condition used to be "the path has a trailing
// slash", so the final directory was never created: `~/.config/udpscope` did not exist, the
// first `save_layout()` failed silently, and the session appeared to persist only on a machine
// where something else had already made the directory.
std::string cur;
for (const char c : path) {
cur.push_back(c);
if (c == '/' || cur.size() == path.size()) {
if (cur == "/" || cur.back() != '/') continue;
::mkdir(cur.c_str(), 0755);
}
for (std::size_t i = 0; i < path.size(); ++i) {
cur.push_back(path[i]);
if (path[i] != '/' && i + 1 != path.size()) continue;
::mkdir(cur.c_str(), 0755);
}
}
@@ -115,6 +117,57 @@ std::string layout_path(const std::string& host, std::uint16_t port) {
return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml";
}
std::string math_node_text(const MathNode& node) {
// The op is written as its index through `math_op_at()`, not as the raw enum value: that accessor
// is the contract the dialog, the engine and this file share, so the format is tied to it rather
// than to the declaration order of an enum nobody else has to know about.
int kind = 0;
for (int i = 0; i < math_op_count(); ++i) {
if (math_op_at(i) == node.op) kind = i;
}
std::ostringstream o;
o << kind << '|' << node.source << '|' << node.in_a << '|' << node.in_b << '|' << node.gain << '|'
<< node.offset << '|' << node.fc << '|' << node.q << '|' << node.lo << '|' << node.hi << '|'
<< node.x << '|' << node.y;
return o.str();
}
bool math_node_from_text(const std::string& text, MathNode& out) {
std::vector<std::string> f;
std::string cur;
for (const char c : text) {
if (c == '|') {
f.push_back(cur);
cur.clear();
} else {
cur.push_back(c);
}
}
f.push_back(cur);
if (f.size() != 12u) return false;
const auto number = [&](std::size_t i, double& v) {
try {
v = std::stod(f[i]);
return true;
} catch (...) {
return false;
}
};
double kind = 0.0;
double in_a = -1.0;
double in_b = -1.0;
if (!number(0, kind) || !number(2, in_a) || !number(3, in_b)) return false;
const int k = static_cast<int>(kind);
if (k < 0 || k >= math_op_count()) return false;
out = MathNode{};
out.op = math_op_at(k);
out.source = f[1];
out.in_a = static_cast<int>(in_a);
out.in_b = static_cast<int>(in_b);
return number(4, out.gain) && number(5, out.offset) && number(6, out.fc) && number(7, out.q) &&
number(8, out.lo) && number(9, out.hi) && number(10, out.x) && number(11, out.y);
}
bool load_layout(const std::string& path, LayoutState& out) {
std::ifstream in(path);
if (!in) return false;
@@ -123,10 +176,13 @@ bool load_layout(const std::string& path, LayoutState& out) {
st.window_s = 0.0;
st.rows.clear();
st.calibration.clear();
st.enabled.clear();
st.math.clear();
st.measures.clear();
std::string line;
// What the last `[[...]]` header opened, so keys land in the right record.
enum class Section { Top, Plot, Calibration } section = Section::Top;
enum class Section { Top, Plot, Calibration, Math, Measure } section = Section::Top;
int plot_count = 0;
bool plot_section_seen = false;
while (std::getline(in, line)) {
@@ -140,6 +196,12 @@ bool load_layout(const std::string& path, LayoutState& out) {
} else if (t == "[[calibration]]") {
section = Section::Calibration;
st.calibration.emplace_back();
} else if (t == "[[math]]") {
section = Section::Math;
st.math.emplace_back();
} else if (t == "[[measure]]") {
section = Section::Measure;
st.measures.emplace_back();
} else {
section = Section::Top; // an unknown table: keep its keys out of ours
}
@@ -168,6 +230,10 @@ bool load_layout(const std::string& path, LayoutState& out) {
st.window_s = std::stod(value);
} catch (...) {
}
} else if (key == "enabled") {
if (value.size() >= 2 && value.front() == '[' && value.back() == ']') {
parse_string_array(value.substr(1, value.size() - 2), st.enabled);
}
} else if (key == "plots") {
// Only a fallback for a file with no [[plot]] sections: the writer emits
// both, and honouring both appended the sections onto the pre-created
@@ -185,6 +251,52 @@ bool load_layout(const std::string& path, LayoutState& out) {
}
st.rows.back().signals = names;
}
} else if (section == Section::Math && !st.math.empty()) {
MathChannel& m = st.math.back();
if (key == "name") {
m.name = strip_quote(value);
} else if (key == "unit") {
m.unit = strip_quote(value);
} else if (key == "enabled") {
m.enabled = value == "true";
} else if (key == "output") {
try {
m.output = std::stoi(value);
} catch (...) {
}
} else if (key == "nodes") {
std::vector<std::string> texts;
if (value.size() >= 2 && value.front() == '[' && value.back() == ']') {
parse_string_array(value.substr(1, value.size() - 2), texts);
}
m.nodes.clear();
for (const std::string& text : texts) {
MathNode node;
// A node this version cannot read is dropped rather than failing the whole file: a graph
// with a hole in it is invalid and the editor says so, which beats losing the session.
if (math_node_from_text(text, node)) m.nodes.push_back(node);
}
}
} else if (section == Section::Measure && !st.measures.empty()) {
Measure& m = st.measures.back();
if (key == "signal") {
m.signal = strip_quote(value);
} else if (key == "type") {
try {
const int index = std::stoi(value);
if (index >= 0 && index < measure_type_count()) m.type = measure_type_at(index);
} catch (...) {
}
} else if (key == "threshold") {
try {
m.options.threshold_pct = std::stod(value);
} catch (...) {
}
} else if (key == "above") {
m.options.above = value == "true";
} else if (key == "show") {
m.options.show_on_plot = value == "true";
}
} else if (section == Section::Calibration && !st.calibration.empty()) {
if (key == "name") {
st.calibration.back().name = strip_quote(value);
@@ -213,6 +325,17 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << "# udpscope session layout (written by the app; safe to edit)\n";
if (state.window_s > 0.0) o << "window_s = " << state.window_s << "\n";
o << "plots = " << state.rows.size() << "\n";
// Top level, before any table. Emitted after the `[[calibration]]` tables it belonged to the last
// of them -- a bare key after a header is that table's key in TOML -- so the parser, correctly,
// never saw it and a session's enable state was silently lost.
if (!state.enabled.empty()) {
o << "enabled = [";
for (std::size_t i = 0; i < state.enabled.size(); ++i) {
if (i != 0) o << ", ";
o << toml_escape(state.enabled[i]);
}
o << "]\n";
}
for (const auto& row : state.rows) {
o << "\n[[plot]]\nsignals = [";
for (std::size_t i = 0; i < row.signals.size(); ++i) {
@@ -225,6 +348,30 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain
<< "\noffset = " << cal.offset << "\n";
}
for (const MathChannel& m : state.math) {
o << "\n[[math]]\nname = " << toml_escape(m.name) << "\nunit = " << toml_escape(m.unit)
<< "\nenabled = " << (m.enabled ? "true" : "false") << "\noutput = " << m.output
<< "\nnodes = [";
for (std::size_t i = 0; i < m.nodes.size(); ++i) {
if (i != 0) o << ", ";
o << toml_escape(math_node_text(m.nodes[i]));
}
o << "]\n";
}
for (const Measure& m : state.measures) {
// The *accessor's* index, not the enum's value: `measure_type_at()` is the mapping the dialog
// and this file share, and its order is the dropdown's -- which is not the declaration order of
// the enum. Writing the raw value and reading it back through the accessor turned `top` into
// whatever sat at that position (the same mistake the math nodes' op index was written to avoid).
int kind = 0;
for (int i = 0; i < measure_type_count(); ++i) {
if (measure_type_at(i) == m.type) kind = i;
}
o << "\n[[measure]]\nsignal = " << toml_escape(m.signal) << "\ntype = " << kind
<< "\nthreshold = " << m.options.threshold_pct
<< "\nabove = " << (m.options.above ? "true" : "false") << "\nshow = "
<< (m.options.show_on_plot ? "true" : "false") << "\n";
}
// A sibling temp file + rename: a crash mid-write leaves the old layout, not a
// truncated one that would fail to parse next start.