added math signals
This commit is contained in:
1 parent
02620ca946
commit
aa2508e431
10 files changed
+1773
-27
No files matched your search
+105
-1
@@ -148,11 +148,18 @@ ScopeModel::ScopeModel(const Config& cfg) : cfg_(cfg) {
|
||||
|
||||
void ScopeModel::set_signals(std::vector<SignalInfo> signals) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
// Keep the enable state across CONFIG updates for signals that kept their name.
|
||||
// Keep the enable state across CONFIG updates for signals that kept their name. The math
|
||||
// channels' synthetic entries carry theirs in `math_`, so they are skipped here -- a CONFIG must
|
||||
// not be able to turn a derived channel off, and must not lose it either (it is re-appended
|
||||
// below).
|
||||
std::unordered_map<std::string, bool> was_enabled;
|
||||
for (std::size_t i = 0; i < signals_.size(); ++i) {
|
||||
if (signals_[i].math) continue;
|
||||
was_enabled.emplace(signals_[i].name, i < enabled_.size() ? enabled_[i] : false);
|
||||
}
|
||||
signals.erase(std::remove_if(signals.begin(), signals.end(),
|
||||
[](const SignalInfo& s) { return s.math; }),
|
||||
signals.end());
|
||||
signals_ = std::move(signals);
|
||||
enabled_.assign(signals_.size(), false);
|
||||
for (std::size_t i = 0; i < signals_.size(); ++i) {
|
||||
@@ -162,17 +169,103 @@ void ScopeModel::set_signals(std::vector<SignalInfo> signals) {
|
||||
// chose for a signal that kept its name.
|
||||
enabled_[i] = it != was_enabled.end() ? it->second : false;
|
||||
}
|
||||
append_math_signals_locked();
|
||||
rebuild_channels();
|
||||
rebuild_locked();
|
||||
generation_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ScopeModel::append_math_signals_locked() {
|
||||
// One synthetic signal per math channel, appended after the stream's own. They go through the
|
||||
// ordinary signal path on purpose: a derived channel is a channel, so the panel's switch, the
|
||||
// plot binding, the measurements and the export all work on it without knowing it is derived.
|
||||
// A `Source` in a graph names a *signal*, so another math channel is a valid source too.
|
||||
math_signal_names_.clear();
|
||||
for (const MathChannel& m : math_) {
|
||||
if (m.name.empty()) continue;
|
||||
SignalInfo sig;
|
||||
sig.name = m.name;
|
||||
sig.unit = m.unit;
|
||||
sig.math = true;
|
||||
sig.sampling_rate = 0.0; // not a streamed signal: the model's own dt applies
|
||||
math_signal_names_.push_back(m.name);
|
||||
signals_.push_back(std::move(sig));
|
||||
enabled_.push_back(m.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void ScopeModel::set_math_channels(std::vector<MathChannel> channels) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
math_ = std::move(channels);
|
||||
// Lift the previous synthetic entries out of the signal list; `append_math_signals_locked()`
|
||||
// puts the current ones back, so an edit is the same kind of structural rebuild as a CONFIG.
|
||||
signals_.erase(std::remove_if(signals_.begin(), signals_.end(),
|
||||
[](const SignalInfo& s) { return s.math; }),
|
||||
signals_.end());
|
||||
enabled_.resize(signals_.size());
|
||||
append_math_signals_locked();
|
||||
rebuild_channels();
|
||||
rebuild_locked();
|
||||
// Prepare the runtimes for the rate this plan chose: a cutoff the user typed in Hz is only
|
||||
// meaningful against the rate the filter will actually run at, and decimation can have changed
|
||||
// it under them.
|
||||
prepare_math_locked();
|
||||
generation_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ScopeModel::prepare_math_locked() {
|
||||
// Called from `rebuild_locked()`, so it runs on **every** plan change -- a CONFIG, an
|
||||
// enable/disable, and a window change that moves the decimation. A biquad's coefficients are a
|
||||
// function of the sample interval, so one prepared for the old rate is a filter with the wrong
|
||||
// cutoff the moment the plan moves; and its state is the old rate's history, which is not a
|
||||
// starting point for the new one. Both are reset here, together, so the two cannot drift apart.
|
||||
math_rt_.assign(math_.size(), MathRuntime{});
|
||||
const double dt = part_.slot_dt();
|
||||
for (std::size_t i = 0; i < math_.size(); ++i) math_prepare(math_[i], math_rt_[i], dt);
|
||||
}
|
||||
|
||||
void ScopeModel::push_math_sample(const std::vector<f32>& sample) {
|
||||
// Values for this slot, extended as the derived ones are computed: a graph may name another math
|
||||
// channel, and nodes are evaluated in list order, so by the time one is asked for it is there.
|
||||
std::vector<f32> values = sample;
|
||||
const auto value_of = [&](const std::string& name) -> double {
|
||||
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||
if (channels_[c].name != name) continue;
|
||||
return c < values.size() ? static_cast<double>(values[c]) : std::nan("");
|
||||
}
|
||||
return std::nan("");
|
||||
};
|
||||
for (std::size_t m = 0; m < math_.size(); ++m) {
|
||||
const int channel = m < math_channel_.size() ? math_channel_[m] : -1;
|
||||
if (channel < 0 || m >= math_rt_.size()) continue;
|
||||
const double out = math_step(math_[m], math_rt_[m], value_of);
|
||||
live().values(static_cast<std::size_t>(channel)).push(static_cast<f32>(out));
|
||||
if (values.size() <= static_cast<std::size_t>(channel)) {
|
||||
values.resize(static_cast<std::size_t>(channel) + 1, 0.0f);
|
||||
}
|
||||
values[static_cast<std::size_t>(channel)] = static_cast<f32>(out);
|
||||
}
|
||||
}
|
||||
|
||||
void ScopeModel::rebuild_channels() {
|
||||
channels_.clear();
|
||||
// Which channel each math channel ended up with, by name: a disabled one has none, and the
|
||||
// producer must not push a derived sample into another channel's ring.
|
||||
math_channel_.assign(math_.size(), -1);
|
||||
const auto math_index_of = [&](const std::string& name) -> int {
|
||||
for (std::size_t k = 0; k < math_.size(); ++k) {
|
||||
if (math_[k].name == name) return static_cast<int>(k);
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
for (std::size_t s = 0; s < signals_.size(); ++s) {
|
||||
const SignalInfo& sig = signals_[s];
|
||||
if (s >= enabled_.size() || !enabled_[s]) continue;
|
||||
if (is_time_signal(signals_, s)) continue;
|
||||
if (sig.math) {
|
||||
const int k = math_index_of(sig.name);
|
||||
if (k >= 0) math_channel_[static_cast<std::size_t>(k)] = static_cast<int>(channels_.size());
|
||||
}
|
||||
ChannelInfo ch;
|
||||
ch.signal = s;
|
||||
ch.enabled = true;
|
||||
@@ -182,6 +275,7 @@ void ScopeModel::rebuild_channels() {
|
||||
// exactly `elements` values per packet, which is the block of samples.
|
||||
ch.name = sig.name;
|
||||
ch.unit = sig.unit;
|
||||
ch.math = sig.math;
|
||||
ch.elements = sig.elements();
|
||||
ch.dt = sig.sampling_rate > 0.0 ? 1.0 / sig.sampling_rate : 0.0;
|
||||
// A calibration the user already set for this name (before it was enabled, or
|
||||
@@ -210,6 +304,7 @@ void ScopeModel::rebuild_locked() {
|
||||
store_ = std::make_unique<BufferStore>(part_, part_.span_s);
|
||||
|
||||
if (trigger_.channel >= static_cast<int>(channels_.size())) trigger_.channel = -1;
|
||||
prepare_math_locked();
|
||||
}
|
||||
|
||||
bool ScopeModel::set_target_span(double seconds) {
|
||||
@@ -262,6 +357,15 @@ void ScopeModel::set_signal_enabled(std::size_t signal, bool on) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (signal >= enabled_.size() || enabled_[signal] == on) return;
|
||||
enabled_[signal] = on;
|
||||
// A math channel's synthetic signal is enabled *here* -- it is the panel's switch -- while
|
||||
// `append_math_signals_locked()` re-states `enabled_` from `math_` on the next edit. The two have
|
||||
// to agree, or an edit would quietly turn a derived channel off (and the rebuild below reads
|
||||
// `enabled_`, so the channel itself would go with it).
|
||||
if (signal < signals_.size() && signals_[signal].math) {
|
||||
for (MathChannel& m : math_) {
|
||||
if (m.name == signals_[signal].name) m.enabled = on;
|
||||
}
|
||||
}
|
||||
// The trigger names a channel by its display name, which is stable across
|
||||
// renumbering: when its own signal is disabled the channel vanishes (the trigger
|
||||
// then waits, and comes back when the signal is re-enabled).
|
||||
|
||||
Reference in new issue
Block a user