diff --git a/debian/bedsidemon.install.in b/debian/bedsidemon.install.in new file mode 100644 index 0000000..b94bf32 --- /dev/null +++ b/debian/bedsidemon.install.in @@ -0,0 +1,5 @@ +# Since we have only 1 package listed in debian/control file, +# the dh_auto_install will take all files installed +# by "make install" automatically, no need to list them here. + +# usr/bin/* diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..eeb22d3 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +bedsidemon (0.0.1) stable; urgency=low + + * Initial release + + -- Ivan Gagis Fri, 23 Feb 2024 13:49:00 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control.in b/debian/control.in new file mode 100644 index 0000000..a207377 --- /dev/null +++ b/debian/control.in @@ -0,0 +1,24 @@ +Source: bedsidemon +Section: utils +Priority: extra +Maintainer: Ivan Gagis +Build-Depends: + debhelper (>= 9), + dpkg-dev (>=1.17.0), + prorab, + prorab-extra, + myci, + clang-tidy, + clang-format, + libtst-dev, + libclargs-dev, + libruisapp-dev +Build-Depends-Indep: doxygen +Standards-Version: 3.9.2 + +Package: bedsidemon +Section: utils +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Bed side monitor demo app. + Demo application of medical bed side monitor. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..7f7425c --- /dev/null +++ b/debian/rules @@ -0,0 +1,16 @@ +#!/usr/bin/make -f + +export PREFIX := /usr + +# dbgsrc_pkg_name := $(filter %-dbgsrc, $(shell awk '/^Package: /{print $2}' debian/control)) +# debug_prefix_map_arg := -fdebug-prefix-map=$(shell pwd)/src=$(PREFIX)/src/$(patsubst %-dbgsrc,%,$(dbgsrc_pkg_name)) + +# export PRORAB_INSTALL_DBGSRC := true + +# export DEB_CFLAGS_MAINT_APPEND := $(debug_prefix_map_arg) +# export DEB_CXXFLAGS_MAINT_APPEND := $(debug_prefix_map_arg) +# export DEB_OBJCFLAGS_MAINT_APPEND := $(debug_prefix_map_arg) +# export DEB_OBJCXXFLAGS_MAINT_APPEND := $(debug_prefix_map_arg) + +%: + dh $@ diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/src/main.cpp b/src/main.cpp index 1e1e9e1..70c79ad 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,11 +19,10 @@ along with this program. If not, see . /* ================ LICENSE END ================ */ +#include #include #include -#include - #include "spo2/contec_cms50d_plus.hpp" #include "spo2/setocare_st_t130_u01.hpp" #include "spo2/spo2_parameter_window.hpp" @@ -95,7 +94,9 @@ const ruisapp::application_factory app_fac([](auto args) { clargs::parser p; - p.add("window","run in window mode", [&](){window = true;}); + p.add("window", "run in window mode", [&]() { + window = true; + }); p.parse(args); diff --git a/src/serial_port.cpp b/src/serial_port.cpp index 967c115..a8907a6 100644 --- a/src/serial_port.cpp +++ b/src/serial_port.cpp @@ -50,16 +50,16 @@ serial_port::serial_port(std::string_view port_filename, baud_rate baud_rate) : }); // TODO: move setting port config to the class's public method - termios newtermios{0}; - newtermios.c_cflag = CBAUD | CS8 | CLOCAL | CREAD; - newtermios.c_iflag = IGNPAR | IGNBRK; - newtermios.c_oflag = 0; - newtermios.c_lflag = 0; + termios newtermios{ + .c_iflag = IGNPAR | IGNBRK, // + .c_oflag = 0, + .c_cflag = CBAUD | CS8 | CLOCAL | CREAD, + .c_lflag = 0 + }; newtermios.c_cc[VMIN] = 0; newtermios.c_cc[VTIME] = 0; - ASSERT(size_t(baud_rate) < size_t(baud_rate::enum_size)) - speed_t br = baud_rate_map[size_t(baud_rate)]; + ASSERT(size_t(baud_rate) < size_t(baud_rate::enum_size)) speed_t br = baud_rate_map[size_t(baud_rate)]; cfsetospeed(&newtermios, br); cfsetispeed(&newtermios, br); diff --git a/src/spo2/contec_cms50d_plus.cpp b/src/spo2/contec_cms50d_plus.cpp index b3d104e..28b8ee9 100644 --- a/src/spo2/contec_cms50d_plus.cpp +++ b/src/spo2/contec_cms50d_plus.cpp @@ -71,6 +71,7 @@ void contec_cms50d_plus::request_live_data(uint32_t cur_ticks) ASSERT(!this->is_sending) this->last_ticks = cur_ticks; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) this->send({0x7d, 0x81, 0xa1, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}); this->is_sending = true; } @@ -82,7 +83,7 @@ void contec_cms50d_plus::feed(uint8_t byte) // ignore any data, it is not supposed to come from disconnected port break; case state::idle: - if (byte & 0x80) { + if (byte & utki::bit_7_mask) { // not a packet type byte, // need to wait for next packet start, // ignore @@ -91,7 +92,7 @@ void contec_cms50d_plus::feed(uint8_t byte) this->handle_packet_type_byte(byte); break; case state::read_packet_high_bits: - if (!(byte & 0x80)) { + if (!(byte & utki::bit_7_mask)) { // not a packet body byte, process it as packet type byte this->state_v = state::idle; this->handle_packet_type_byte(byte); @@ -182,20 +183,18 @@ void contec_cms50d_plus::handle_packet() if (this->packet_v.type == packet_type::live_data) { ASSERT(this->packet_v.buffer.size() == live_data_packet_size) - live_data data; - - data.signal_strength = this->packet_v.buffer[0] & utki::lower_nibble_mask; - data.searching_time_too_long = (this->packet_v.buffer[0] & utki::bit_4_mask) != 0; - data.pulse_beep = (this->packet_v.buffer[0] & utki::bit_6_mask) != 0; - data.finger_out = (this->packet_v.buffer[0] & utki::bit_7_mask) != 0; - - data.waveform_point = this->packet_v.buffer[1] & (~utki::bit_7_mask); - data.searching_pulse = (this->packet_v.buffer[1] & utki::bit_7_mask) != 0; - data.is_pi_data_valid = (this->packet_v.buffer[2] & utki::bit_4_mask) == 0; - - data.pulse_rate = this->packet_v.buffer[3]; - data.spo2 = this->packet_v.buffer[4]; - data.pi = utki::deserialize16le(&this->packet_v.buffer[5]); + live_data data{ + .signal_strength = uint8_t(this->packet_v.buffer[0] & utki::lower_nibble_mask), + .searching_time_too_long = (this->packet_v.buffer[0] & utki::bit_4_mask) != 0, + .pulse_beep = (this->packet_v.buffer[0] & utki::bit_6_mask) != 0, + .finger_out = (this->packet_v.buffer[0] & utki::bit_7_mask) != 0, + .searching_pulse = (this->packet_v.buffer[1] & utki::bit_7_mask) != 0, + .is_pi_data_valid = (this->packet_v.buffer[2] & utki::bit_4_mask) == 0, + .waveform_point = uint8_t(this->packet_v.buffer[1] & (~utki::bit_7_mask)), + .pulse_rate = this->packet_v.buffer[3], + .spo2 = this->packet_v.buffer[4], + .pi = utki::deserialize16le(&this->packet_v.buffer[5]), // NOLINT(cppcoreguidelines-avoid-magic-numbers) + }; // std::cout << "signal_strength = " << unsigned(data.signal_strength) << "\n"; // std::cout << "\t" << "searching_time_too_long = " << data.searching_time_too_long << "\n"; @@ -210,16 +209,20 @@ void contec_cms50d_plus::handle_packet() // std::cout << std::endl; uint32_t cur_ticks = utki::get_ticks_ms(); - uint16_t delta_time = uint16_t(cur_ticks - this->last_ticks); + auto delta_time = uint16_t(cur_ticks - this->last_ticks); this->last_ticks = cur_ticks; using std::min; using std::max; + constexpr auto max_signal_strength = 10; + constexpr auto min_signal_strength = 4; + this->push(spo2_measurement{ .signal_strength = uint8_t( - (min(max(int(data.signal_strength), 4), 10) - 4) * std::centi::den / 6 - ), // value is from [4, 10] + (min(max(int(data.signal_strength), min_signal_strength), max_signal_strength) - min_signal_strength) * + std::centi::den / (max_signal_strength - min_signal_strength) + ), .pulse_beat = data.pulse_beep, .finger_out = data.finger_out, .waveform_point = float(data.waveform_point), @@ -229,11 +232,14 @@ void contec_cms50d_plus::handle_packet() .delta_time_ms = delta_time }); + constexpr auto sample_rate = 60; + constexpr auto acquisition_time_sec = 30; + // CMS50D+ has limitation of sending live data for only 30 seconds after it was requested. // Workaround this limitation by requesting live data every ~15 seconds, assuming that it sends // about 60 live data packets per second. ++this->num_live_data_packages_received; - if (this->num_live_data_packages_received > 60 * 15) { + if (this->num_live_data_packages_received > sample_rate * (acquisition_time_sec / 2)) { this->num_live_data_packages_received = 0; this->request_live_data(cur_ticks); } diff --git a/src/spo2/contec_cms50d_plus.hpp b/src/spo2/contec_cms50d_plus.hpp index 2752f9f..da3fbc0 100644 --- a/src/spo2/contec_cms50d_plus.hpp +++ b/src/spo2/contec_cms50d_plus.hpp @@ -49,9 +49,9 @@ class contec_cms50d_plus : }; struct packet { - uint8_t high_bits; - packet_type type; - size_t num_bytes_to_read; + uint8_t high_bits{}; + packet_type type{packet_type::enum_size}; + size_t num_bytes_to_read{}; std::vector buffer; } packet_v; @@ -59,11 +59,18 @@ class contec_cms50d_plus : unsigned num_live_data_packages_received = 0; - uint32_t last_ticks; + uint32_t last_ticks{}; public: contec_cms50d_plus(utki::shared_ref pw, std::string_view port_filename); - ~contec_cms50d_plus(); + + contec_cms50d_plus(const contec_cms50d_plus&) = delete; + contec_cms50d_plus& operator=(const contec_cms50d_plus&) = delete; + + contec_cms50d_plus(contec_cms50d_plus&&) = delete; + contec_cms50d_plus& operator=(contec_cms50d_plus&&) = delete; + + ~contec_cms50d_plus() override; private: void on_data_received(utki::span data) override; diff --git a/src/spo2/setocare_st_t130_u01.cpp b/src/spo2/setocare_st_t130_u01.cpp index 07446d9..e952b30 100644 --- a/src/spo2/setocare_st_t130_u01.cpp +++ b/src/spo2/setocare_st_t130_u01.cpp @@ -35,8 +35,6 @@ setocare_st_t130_u01::setocare_st_t130_u01(utki::shared_refstate_v = state::wait_packet_first_byte; - this->start(); } @@ -143,18 +141,19 @@ void setocare_st_t130_u01::handle_packet() // std::cout << std::endl; uint32_t cur_ticks = utki::get_ticks_ms(); - uint16_t delta_time = uint16_t(cur_ticks - this->last_ticks); + auto delta_time = uint16_t(cur_ticks - this->last_ticks); this->last_ticks = cur_ticks; using std::min; constexpr uint8_t max_signal_strength = 8; + constexpr auto max_pleth_value = 100; this->push(spo2_measurement{ .signal_strength = min(signal_strength, max_signal_strength), .pulse_beat = pulse_beep, .finger_out = no_finger, - .waveform_point = pleth > 100 ? 50 : float(pleth), + .waveform_point = float(pleth > max_pleth_value ? max_pleth_value / 2 : pleth), .pulse_rate = pulse_rate, .spo2 = spo2, .perfusion_index = 0, diff --git a/src/spo2/setocare_st_t130_u01.hpp b/src/spo2/setocare_st_t130_u01.hpp index 8c475f3..30df83f 100644 --- a/src/spo2/setocare_st_t130_u01.hpp +++ b/src/spo2/setocare_st_t130_u01.hpp @@ -39,18 +39,25 @@ class setocare_st_t130_u01 : enum_size }; - state state_v = state::disconnected; + state state_v = state::wait_packet_first_byte; struct packet { - size_t num_bytes_to_read; + size_t num_bytes_to_read{}; std::vector buffer; } packet_v; - uint32_t last_ticks; + uint32_t last_ticks{}; public: setocare_st_t130_u01(utki::shared_ref pw, std::string_view port_filename); - ~setocare_st_t130_u01(); + + setocare_st_t130_u01(const setocare_st_t130_u01&) = delete; + setocare_st_t130_u01& operator=(const setocare_st_t130_u01&) = delete; + + setocare_st_t130_u01(setocare_st_t130_u01&&) = delete; + setocare_st_t130_u01& operator=(setocare_st_t130_u01&&) = delete; + + ~setocare_st_t130_u01() override; private: void on_data_received(utki::span data) override; diff --git a/src/spo2/spo2_parameter_window.cpp b/src/spo2/spo2_parameter_window.cpp index 5edc406..7f2be93 100644 --- a/src/spo2/spo2_parameter_window.cpp +++ b/src/spo2/spo2_parameter_window.cpp @@ -21,6 +21,7 @@ along with this program. If not, see . #include "spo2_parameter_window.hpp" +#include #include #include @@ -41,10 +42,12 @@ std::vector> build_layout(utki::shared_ref> build_layout(utki::shared_ref con void spo2_parameter_window::set(const spo2_measurement& meas) { // set oxygenation - if (meas.spo2 == 0 || meas.spo2 > 100) { + if (meas.spo2 == 0 || meas.spo2 > std::centi::den) { // invalid value this->spo2_value.set_text("---"); } else { this->spo2_value.set_text(std::to_string(unsigned(meas.spo2))); } + constexpr auto bpm_invalid_value = 0xff; + // set bpm - if (meas.pulse_rate == 0 || meas.pulse_rate == 0xff) { + if (meas.pulse_rate == 0 || meas.pulse_rate == bpm_invalid_value) { // invalid value this->bpm_value.set_text("---"); } else { this->bpm_value.set_text(std::to_string(unsigned(meas.pulse_rate))); } - this->waveform.push(meas.waveform_point, meas.delta_time_ms); + this->waveform.push(meas.waveform_point, meas.delta_time_ms); } diff --git a/src/spo2/spo2_sensor.hpp b/src/spo2/spo2_sensor.hpp index a68654b..967fed8 100644 --- a/src/spo2/spo2_sensor.hpp +++ b/src/spo2/spo2_sensor.hpp @@ -35,6 +35,12 @@ class spo2_sensor public: spo2_sensor(utki::shared_ref pw); + spo2_sensor(const spo2_sensor&) = delete; + spo2_sensor& operator=(const spo2_sensor&) = delete; + + spo2_sensor(spo2_sensor&&) = delete; + spo2_sensor& operator=(spo2_sensor&&) = delete; + virtual ~spo2_sensor() = default; protected: diff --git a/src/waveform.cpp b/src/waveform.cpp index 8b357a5..03f1d18 100644 --- a/src/waveform.cpp +++ b/src/waveform.cpp @@ -1,7 +1,34 @@ +/* +bedsidemon - Bed-side monitor example GUI project + +Copyright (C) 2024 Ivan Gagis + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +/* ================ LICENSE END ================ */ + #include "waveform.hpp" +#include + using namespace bedsidemon; +namespace { +constexpr auto default_max_value = 100; +} // namespace + waveform::waveform( utki::shared_ref context, ruis::widget::parameters widget_params, @@ -9,71 +36,71 @@ waveform::waveform( ) : ruis::widget(std::move(context), std::move(widget_params)), ruis::color_widget(this->context, std::move(color_params)), - paths{{ - {.vao{this->context.get().renderer}}, - {.vao{this->context.get().renderer}} - }} + paths{ + {// + {.vao{this->context.get().renderer}}, + {.vao{this->context.get().renderer}} + } +}, + value_max(default_max_value) { - this->value_offset = 0; - this->value_max = 100; - constexpr auto default_sweep_speed_mm_per_sec = 100; // this->px_per_ms = this->context.get().units.mm_to_px(default_sweep_speed_mm_per_sec / 1000.0); - this->px_per_ms = default_sweep_speed_mm_per_sec / 1000.0; + this->px_per_ms = ruis::real(default_sweep_speed_mm_per_sec) / std::milli::den; - constexpr auto default_gap_pp = 30; - this->gap_px = this->context.get().units.pp_to_px(default_gap_pp); + constexpr auto default_gap_pp = 30; + this->gap_px = this->context.get().units.pp_to_px(default_gap_pp); } -void waveform::render(const ruis::matrix4& matrix)const { - for(const auto& pv : this->paths){ +void waveform::render(const ruis::matrix4& matrix) const +{ + for (const auto& pv : this->paths) { pv.vao.render(ruis::matrix4(matrix).translate(pv.origin), this->get_color()); } } -void waveform::on_resize(){ - for(auto& p : this->paths){ +void waveform::on_resize() +{ + for (auto& p : this->paths) { p.points.clear(); } this->make_vaos(); } -void waveform::push(ruis::real value, ruis::real dt_ms){ +void waveform::push(ruis::real value, ruis::real dt_ms) +{ auto dx = dt_ms * this->px_per_ms; // dx can be 0 when it is a very first sample received from sensor, dt_ms == 0 in this case and => dx == 0 - ASSERT(dx >= 0, [&](auto&o){o << "dx = " << dx << ", dt_ms = " << dt_ms << ", this->px_per_ms = " << this->px_per_ms;}) + ASSERT(dx >= 0, [&](auto& o) { + o << "dx = " << dx << ", dt_ms = " << dt_ms << ", this->px_per_ms = " << this->px_per_ms; + }) // push new point - if(this->paths[0].points.empty()){ - this->paths[0].points.push_back({0 , value}); + if (this->paths[0].points.empty()) { + this->paths[0].points.push_back({0, value}); return; - }else{ - if(dx == 0){ + } else { + if (dx == 0) { // if first sample received from sensor we don't know the delta time from previous sample, // so just update the latest value this->paths[0].points.back().y() = value; return; - }else{ - this->paths[0].points.push_back( - { - this->paths[0].points.back().x() + dx, - value - } - ); + } else { + this->paths[0].points.push_back({this->paths[0].points.back().x() + dx, value}); } } ASSERT(!this->paths[0].points.empty()) // wrap around paths[0].points if needed - if(this->paths[0].points.back().x() >= this->rect().d.x()){ + if (this->paths[0].points.back().x() >= this->rect().d.x()) { auto dx1 = this->paths[0].points.back().x() - this->rect().d.x(); auto dx2 = dx - dx1; ASSERT(dx1 >= 0) ASSERT(dx2 >= 0) auto ratio = dx1 / dx; - + ASSERT(this->paths[0].points.size() >= 2) // TODO: why? auto v = this->paths[0].points.back().y(); @@ -98,26 +125,28 @@ void waveform::push(ruis::real value, ruis::real dt_ms){ // pop points from another path auto& pop_path = [&]() -> path& { - if(pop_pos >= this->rect().d.x()){ + if (pop_pos >= this->rect().d.x()) { pop_pos -= this->rect().d.x(); return this->paths[0]; } return this->paths[1]; }(); - if(!pop_path.points.empty()){ + if (!pop_path.points.empty()) { // there should be at least one line segment ASSERT(pop_path.points.size() >= 2) - for(; pop_path.points.size() >= 2;){ - if(std::next(pop_path.points.begin())->x() <= pop_pos){ + for (; pop_path.points.size() >= 2;) { + if (std::next(pop_path.points.begin())->x() <= pop_pos) { pop_path.points.pop_front(); continue; - }else{ + } else { auto tail_x = pop_path.points.front().x(); auto tail_dx = std::next(pop_path.points.begin())->x() - tail_x; - ASSERT(tail_dx > 0, [&](auto&o){o << "tail_dx = " << tail_dx << ", pop_path.size() = " << pop_path.points.size();}) - + ASSERT(tail_dx > 0, [&](auto& o) { + o << "tail_dx = " << tail_dx << ", pop_path.size() = " << pop_path.points.size(); + }) + auto ratio = (pop_pos - tail_x) / tail_dx; auto tail_dv = std::next(pop_path.points.begin())->y() - pop_path.points.front().y(); @@ -126,47 +155,43 @@ void waveform::push(ruis::real value, ruis::real dt_ms){ pop_path.points.front() += ruis::vector2{dx, dv1}; // it is possible that due to floating point calculation errors points coincide - if(pop_path.points.front().x() >= std::next(pop_path.points.begin())->x()){ + if (pop_path.points.front().x() >= std::next(pop_path.points.begin())->x()) { pop_path.points.pop_front(); } break; } } - if(pop_path.points.size() <= 1){ + if (pop_path.points.size() <= 1) { pop_path.points.clear(); } } - // std::cout << "num_left = " << this->paths[0].points.size() << ", num_right = " << this->paths[1].points.size() << std::endl; + // std::cout << "num_left = " << this->paths[0].points.size() << ", num_right = " << this->paths[1].points.size() << + // std::endl; this->make_vaos(); } -void waveform::make_vaos(){ +void waveform::make_vaos() +{ ASSERT(this->value_max > this->value_offset) const auto& height = this->rect().d.y() - 1; // -1 due to line width auto scale = height / (this->value_max - this->value_offset); - for(auto& pv : this->paths){ - if(pv.points.empty()){ + for (auto& pv : this->paths) { + if (pv.points.empty()) { continue; } - pv.origin = { - pv.points.front().x(), - height - pv.points.front().y() * scale + this->value_offset - }; + pv.origin = {pv.points.front().x(), height - pv.points.front().y() * scale + this->value_offset}; ruis::path path; - for(const auto& p : utki::skip_front<1>(pv.points)){ - ruis::vector2 point = { - p.x(), - height - p.y() * scale + this->value_offset - }; + for (const auto& p : utki::skip_front<1>(pv.points)) { + ruis::vector2 point = {p.x(), height - p.y() * scale + this->value_offset}; path.line_to(point - pv.origin); } - pv.vao.set(path.stroke(0.5)); + pv.vao.set(path.stroke()); } } diff --git a/src/waveform.hpp b/src/waveform.hpp index f9a3d29..612bd34 100644 --- a/src/waveform.hpp +++ b/src/waveform.hpp @@ -1,3 +1,24 @@ +/* +bedsidemon - Bed-side monitor example GUI project + +Copyright (C) 2024 Ivan Gagis + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +/* ================ LICENSE END ================ */ + #pragma once #include @@ -12,7 +33,7 @@ class waveform : virtual public ruis::widget, // public ruis::color_widget { - struct path{ + struct path { ruis::path_vao vao; ruis::vector2 origin; @@ -21,12 +42,12 @@ class waveform : std::array paths; - ruis::real value_offset; + ruis::real value_offset{0}; ruis::real value_max; ruis::real px_per_ms; - ruis::real gap_px; + ruis::real gap_px; public: waveform( @@ -37,7 +58,7 @@ class waveform : void render(const ruis::matrix4& matrix) const override; - void on_resize()override; + void on_resize() override; void push(ruis::real value, ruis::real dt_ms);