diff --git a/.gitignore b/.gitignore index 79587d1d0..5fc18bb07 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,6 @@ selfdrive/logcatd/logcatd selfdrive/mapd/default_speeds_by_region.json selfdrive/proclogd/proclogd selfdrive/ui/_ui -selfdrive/ui/_soundd selfdrive/test/longitudinal_maneuvers/out selfdrive/visiond/visiond selfdrive/loggerd/loggerd diff --git a/release/files_common b/release/files_common index d83e074ea..bf8b22369 100644 --- a/release/files_common +++ b/release/files_common @@ -340,7 +340,10 @@ selfdrive/ui/*.h selfdrive/ui/ui selfdrive/ui/text selfdrive/ui/spinner -selfdrive/ui/soundd +selfdrive/ui/soundd/*.cc +selfdrive/ui/soundd/*.h +selfdrive/ui/soundd/soundd +selfdrive/ui/soundd/.gitignore selfdrive/ui/qt/*.cc selfdrive/ui/qt/*.h diff --git a/selfdrive/hardware/base.h b/selfdrive/hardware/base.h index d9ce5c48c..05e55cc03 100644 --- a/selfdrive/hardware/base.h +++ b/selfdrive/hardware/base.h @@ -6,8 +6,8 @@ // no-op base hw class class HardwareNone { public: - static constexpr float MAX_VOLUME = 0; - static constexpr float MIN_VOLUME = 0; + static constexpr float MAX_VOLUME = 0.7; + static constexpr float MIN_VOLUME = 0.2; static std::string get_os_version() { return ""; } diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 8dc010e55..8fc07a06e 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -19,7 +19,7 @@ procs = [ NativeProcess("sensord", "selfdrive/sensord", ["./sensord"], enabled=not PC, persistent=EON, sigkill=EON), NativeProcess("ubloxd", "selfdrive/locationd", ["./ubloxd"], enabled=(not PC or WEBCAM)), NativeProcess("ui", "selfdrive/ui", ["./ui"], persistent=True, watchdog_max_dt=(5 if TICI else None)), - NativeProcess("soundd", "selfdrive/ui", ["./soundd"]), + NativeProcess("soundd", "selfdrive/ui/soundd", ["./soundd"]), NativeProcess("locationd", "selfdrive/locationd", ["./locationd"]), NativeProcess("boardd", "selfdrive/boardd", ["./boardd"], enabled=False), PythonProcess("calibrationd", "selfdrive.locationd.calibrationd"), diff --git a/selfdrive/ui/.gitignore b/selfdrive/ui/.gitignore index 9e529b5d0..5d656f889 100644 --- a/selfdrive/ui/.gitignore +++ b/selfdrive/ui/.gitignore @@ -3,7 +3,6 @@ moc_* watch3 installer/installers/* -tests/playsound replay/replay replay/tests/test_replay qt/text diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 943c072a6..d4c8fe71e 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -44,9 +44,10 @@ qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, asset_obj = qt_env.Object("assets", assets) # build soundd -qt_env.Program("_soundd", "soundd.cc", LIBS=qt_libs) +qt_env.Program("soundd/_soundd", ["soundd/main.cc", "soundd/sound.cc"], LIBS=qt_libs) if GetOption('test'): qt_env.Program("tests/playsound", "tests/playsound.cc", LIBS=base_libs) + qt_env.Program('tests/test_sound', ['tests/test_runner.cc', 'soundd/sound.cc', 'tests/test_sound.cc'], LIBS=[base_libs]) qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 7c3a610ee..a2195a621 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -40,25 +40,12 @@ OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) { } void OnroadWindow::updateState(const UIState &s) { - SubMaster &sm = *(s.sm); QColor bgColor = bg_colors[s.status]; - if (sm.updated("controlsState")) { - const cereal::ControlsState::Reader &cs = sm["controlsState"].getControlsState(); - alerts->updateAlert({QString::fromStdString(cs.getAlertText1()), - QString::fromStdString(cs.getAlertText2()), - QString::fromStdString(cs.getAlertType()), - cs.getAlertSize(), cs.getAlertSound()}, bgColor); - } else if ((sm.frame - s.scene.started_frame) > 5 * UI_FREQ) { - // Handle controls timeout - if (sm.rcv_frame("controlsState") < s.scene.started_frame) { - // car is started, but controlsState hasn't been seen at all - alerts->updateAlert(CONTROLS_WAITING_ALERT, bgColor); - } else if ((nanos_since_boot() - sm.rcv_time("controlsState")) / 1e9 > CONTROLS_TIMEOUT) { - // car is started, but controls is lagging or died - bgColor = bg_colors[STATUS_ALERT]; - alerts->updateAlert(CONTROLS_UNRESPONSIVE_ALERT, bgColor); - } + Alert alert = Alert::get(*(s.sm), s.scene.started_frame); + if (alert.type == "controlsUnresponsive") { + bgColor = bg_colors[STATUS_ALERT]; } + alerts->updateAlert(alert, bgColor); if (bg != bgColor) { // repaint border bg = bgColor; diff --git a/selfdrive/ui/soundd.cc b/selfdrive/ui/soundd.cc deleted file mode 100644 index 5da33831b..000000000 --- a/selfdrive/ui/soundd.cc +++ /dev/null @@ -1,119 +0,0 @@ -#include - -#include - -#include -#include -#include - -#include "selfdrive/ui/qt/util.h" -#include "cereal/messaging/messaging.h" -#include "selfdrive/common/util.h" -#include "selfdrive/hardware/hw.h" -#include "selfdrive/ui/ui.h" - -// TODO: detect when we can't play sounds -// TODO: detect when we can't display the UI - -void sigHandler(int s) { - qApp->quit(); -} - -class Sound : public QObject { -public: - explicit Sound(QObject *parent = 0) { - // TODO: merge again and add EQ in the amp config - const QString sound_asset_path = Hardware::TICI() ? "../assets/sounds_tici/" : "../assets/sounds/"; - std::tuple sound_list[] = { - {AudibleAlert::CHIME_DISENGAGE, sound_asset_path + "disengaged.wav", false}, - {AudibleAlert::CHIME_ENGAGE, sound_asset_path + "engaged.wav", false}, - {AudibleAlert::CHIME_WARNING1, sound_asset_path + "warning_1.wav", false}, - {AudibleAlert::CHIME_WARNING2, sound_asset_path + "warning_2.wav", false}, - {AudibleAlert::CHIME_WARNING2_REPEAT, sound_asset_path + "warning_2.wav", true}, - {AudibleAlert::CHIME_WARNING_REPEAT, sound_asset_path + "warning_repeat.wav", true}, - {AudibleAlert::CHIME_ERROR, sound_asset_path + "error.wav", false}, - {AudibleAlert::CHIME_PROMPT, sound_asset_path + "error.wav", false} - }; - for (auto &[alert, fn, loops] : sound_list) { - QSoundEffect *s = new QSoundEffect(this); - QObject::connect(s, &QSoundEffect::statusChanged, this, &Sound::checkStatus); - s->setSource(QUrl::fromLocalFile(fn)); - sounds[alert] = {s, loops ? QSoundEffect::Infinite : 0}; - } - - sm = new SubMaster({"carState", "controlsState"}); - - QTimer *timer = new QTimer(this); - QObject::connect(timer, &QTimer::timeout, this, &Sound::update); - timer->start(); - }; - ~Sound() { - delete sm; - }; - -private slots: - void checkStatus() { - QSoundEffect *s = qobject_cast(sender()); - assert(s->status() != QSoundEffect::Error); - } - - void update() { - sm->update(100); - if (sm->updated("carState")) { - // scale volume with speed - volume = util::map_val((*sm)["carState"].getCarState().getVEgo(), 0.f, 20.f, - Hardware::MIN_VOLUME, Hardware::MAX_VOLUME); - for (auto &[s, loops] : sounds) { - s->setVolume(std::round(100 * volume) / 100); - } - } - if (sm->updated("controlsState")) { - const cereal::ControlsState::Reader &cs = (*sm)["controlsState"].getControlsState(); - setAlert({QString::fromStdString(cs.getAlertText1()), - QString::fromStdString(cs.getAlertText2()), - QString::fromStdString(cs.getAlertType()), - cs.getAlertSize(), cs.getAlertSound()}); - } else if (sm->rcv_frame("controlsState") > 0 && (*sm)["controlsState"].getControlsState().getEnabled() && - ((nanos_since_boot() - sm->rcv_time("controlsState")) / 1e9 > CONTROLS_TIMEOUT)) { - setAlert(CONTROLS_UNRESPONSIVE_ALERT); - } - } - - void setAlert(Alert a) { - if (!alert.equal(a)) { - alert = a; - // stop sounds - for (auto &[s, loops] : sounds) { - // Only stop repeating sounds - if (s->loopsRemaining() == QSoundEffect::Infinite) { - s->stop(); - } - } - - // play sound - if (alert.sound != AudibleAlert::NONE) { - auto &[s, loops] = sounds[alert.sound]; - s->setLoopCount(loops); - s->play(); - } - } - } - -private: - Alert alert; - float volume = Hardware::MIN_VOLUME; - QMap> sounds; - SubMaster *sm; -}; - -int main(int argc, char **argv) { - qInstallMessageHandler(swagLogMessageHandler); - setpriority(PRIO_PROCESS, 0, -20); - - QApplication a(argc, argv); - std::signal(SIGINT, sigHandler); - std::signal(SIGTERM, sigHandler); - - Sound sound; - return a.exec(); -} diff --git a/selfdrive/ui/soundd/.gitignore b/selfdrive/ui/soundd/.gitignore new file mode 100644 index 000000000..c47f949d3 --- /dev/null +++ b/selfdrive/ui/soundd/.gitignore @@ -0,0 +1 @@ +_soundd diff --git a/selfdrive/ui/soundd/main.cc b/selfdrive/ui/soundd/main.cc new file mode 100644 index 000000000..64088deff --- /dev/null +++ b/selfdrive/ui/soundd/main.cc @@ -0,0 +1,22 @@ +#include + +#include + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/soundd/sound.h" + +void sigHandler(int s) { + qApp->quit(); +} + +int main(int argc, char **argv) { + qInstallMessageHandler(swagLogMessageHandler); + setpriority(PRIO_PROCESS, 0, -20); + + QApplication a(argc, argv); + std::signal(SIGINT, sigHandler); + std::signal(SIGTERM, sigHandler); + + Sound sound; + return a.exec(); +} diff --git a/selfdrive/ui/soundd/sound.cc b/selfdrive/ui/soundd/sound.cc new file mode 100644 index 000000000..dec46cbea --- /dev/null +++ b/selfdrive/ui/soundd/sound.cc @@ -0,0 +1,58 @@ +#include "selfdrive/ui/soundd/sound.h" + +#include "cereal/messaging/messaging.h" +#include "selfdrive/common/util.h" + +// TODO: detect when we can't play sounds +// TODO: detect when we can't display the UI + +Sound::Sound(QObject *parent) : sm({"carState", "controlsState"}) { + const QString sound_asset_path = Hardware::TICI() ? "../../assets/sounds_tici/" : "../../assets/sounds/"; + for (auto &[alert, fn, loops] : sound_list) { + QSoundEffect *s = new QSoundEffect(this); + QObject::connect(s, &QSoundEffect::statusChanged, [=]() { + assert(s->status() != QSoundEffect::Error); + }); + s->setSource(QUrl::fromLocalFile(sound_asset_path + fn)); + s->setVolume(Hardware::MIN_VOLUME); + sounds[alert] = {s, loops ? QSoundEffect::Infinite : 0}; + } + + QTimer *timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, this, &Sound::update); + timer->start(1000 / UI_FREQ); +}; + +void Sound::update() { + sm.update(0); + if (sm.updated("carState")) { + // scale volume with speed + float volume = util::map_val(sm["carState"].getCarState().getVEgo(), 0.f, 20.f, + Hardware::MIN_VOLUME, Hardware::MAX_VOLUME); + for (auto &[s, loops] : sounds) { + s->setVolume(std::round(100 * volume) / 100); + } + } + + setAlert(Alert::get(sm, 1)); +} + +void Sound::setAlert(const Alert &alert) { + if (!current_alert.equal(alert)) { + current_alert = alert; + // stop sounds + for (auto &[s, loops] : sounds) { + // Only stop repeating sounds + if (s->loopsRemaining() == QSoundEffect::Infinite) { + s->stop(); + } + } + + // play sound + if (alert.sound != AudibleAlert::NONE) { + auto &[s, loops] = sounds[alert.sound]; + s->setLoopCount(loops); + s->play(); + } + } +} diff --git a/selfdrive/ui/soundd/sound.h b/selfdrive/ui/soundd/sound.h new file mode 100644 index 000000000..0fb1dd5b4 --- /dev/null +++ b/selfdrive/ui/soundd/sound.h @@ -0,0 +1,30 @@ +#include +#include +#include + +#include "selfdrive/hardware/hw.h" +#include "selfdrive/ui/ui.h" + +const std::tuple sound_list[] = { + {AudibleAlert::CHIME_DISENGAGE, "disengaged.wav", false}, + {AudibleAlert::CHIME_ENGAGE, "engaged.wav", false}, + {AudibleAlert::CHIME_WARNING1, "warning_1.wav", false}, + {AudibleAlert::CHIME_WARNING2, "warning_2.wav", false}, + {AudibleAlert::CHIME_WARNING2_REPEAT, "warning_2.wav", true}, + {AudibleAlert::CHIME_WARNING_REPEAT, "warning_repeat.wav", true}, + {AudibleAlert::CHIME_ERROR, "error.wav", false}, + {AudibleAlert::CHIME_PROMPT, "error.wav", false}, +}; + +class Sound : public QObject { +public: + explicit Sound(QObject *parent = 0); + +protected: + void update(); + void setAlert(const Alert &alert); + + Alert current_alert = {}; + QMap> sounds; + SubMaster sm; +}; diff --git a/selfdrive/ui/soundd b/selfdrive/ui/soundd/soundd similarity index 100% rename from selfdrive/ui/soundd rename to selfdrive/ui/soundd/soundd diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index b80a5b49c..7765bab17 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -1,2 +1,3 @@ test -play_sound +playsound +test_sound diff --git a/selfdrive/ui/tests/test_runner.cc b/selfdrive/ui/tests/test_runner.cc new file mode 100644 index 000000000..b20ac86c6 --- /dev/null +++ b/selfdrive/ui/tests/test_runner.cc @@ -0,0 +1,10 @@ +#define CATCH_CONFIG_RUNNER +#include "catch2/catch.hpp" +#include + +int main(int argc, char **argv) { + // unit tests for Qt + QCoreApplication app(argc, argv); + const int res = Catch::Session().run(argc, argv); + return (res < 0xff ? res : 0xff); +} diff --git a/selfdrive/ui/tests/test_sound.cc b/selfdrive/ui/tests/test_sound.cc new file mode 100644 index 000000000..579867ad2 --- /dev/null +++ b/selfdrive/ui/tests/test_sound.cc @@ -0,0 +1,63 @@ +#include +#include +#include + +#include "catch2/catch.hpp" +#include "selfdrive/ui/soundd/sound.h" + +class TestSound : public Sound { +public: + TestSound() : Sound() { + for (auto i = sounds.constBegin(); i != sounds.constEnd(); ++i) { + QObject::connect(i.value().first, &QSoundEffect::playingChanged, [=, s = i.value().first, a = i.key()]() { + if (s->isPlaying()) { + bool repeat = a == AudibleAlert::CHIME_WARNING_REPEAT || a == AudibleAlert::CHIME_WARNING2_REPEAT; + REQUIRE((s->loopsRemaining() == repeat ? QSoundEffect::Infinite : 1)); + sound_stats[a].first++; + } else { + sound_stats[a].second++; + } + }); + } + } + + QMap> sound_stats; +}; + +void controls_thread(int loop_cnt) { + PubMaster pm({"controlsState"}); + const int DT_CTRL = 10; // ms + for (int i = 0; i < loop_cnt; ++i) { + for (auto &[alert, fn, loops] : sound_list) { + printf("testing %s\n", qPrintable(fn)); + for (int j = 0; j < 1000 / DT_CTRL; ++j) { + MessageBuilder msg; + auto cs = msg.initEvent().initControlsState(); + cs.setAlertSound(alert); + cs.setAlertType(fn.toStdString()); + pm.send("controlsState", msg); + QThread::msleep(DT_CTRL); + } + } + } + QThread::currentThread()->quit(); +} + +TEST_CASE("test sound") { + QEventLoop loop; + + TestSound test_sound; + + const int test_loop_cnt = 2; + QThread t; + QObject::connect(&t, &QThread::started, [=]() { controls_thread(test_loop_cnt); }); + QObject::connect(&t, &QThread::finished, [&]() { loop.quit(); }); + t.start(); + + loop.exec(); + + for (auto [play, stop] : test_sound.sound_stats) { + REQUIRE(play == test_loop_cnt); + REQUIRE(stop == test_loop_cnt); + } +} diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 6bea6ad61..611af5487 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -26,6 +26,12 @@ #define COLOR_YELLOW nvgRGBA(218, 202, 37, 255) #define COLOR_RED nvgRGBA(201, 34, 49, 255) +const int bdr_s = 30; +const int header_h = 420; +const int footer_h = 280; + +const int UI_FREQ = 20; // Hz + typedef cereal::CarControl::HUDControl::AudibleAlert AudibleAlert; // TODO: this is also hardcoded in common/transformations/camera.py @@ -44,7 +50,7 @@ typedef struct Rect { } } Rect; -typedef struct Alert { +struct Alert { QString text1; QString text2; QString type; @@ -53,22 +59,31 @@ typedef struct Alert { bool equal(const Alert &a2) { return text1 == a2.text1 && text2 == a2.text2 && type == a2.type; } -} Alert; -const Alert CONTROLS_WAITING_ALERT = {"openpilot Unavailable", "Waiting for controls to start", - "controlsWaiting", cereal::ControlsState::AlertSize::MID, - AudibleAlert::NONE}; - -const Alert CONTROLS_UNRESPONSIVE_ALERT = {"TAKE CONTROL IMMEDIATELY", "Controls Unresponsive", - "controlsUnresponsive", cereal::ControlsState::AlertSize::FULL, - AudibleAlert::CHIME_WARNING_REPEAT}; -const int CONTROLS_TIMEOUT = 5; - -const int bdr_s = 30; -const int header_h = 420; -const int footer_h = 280; - -const int UI_FREQ = 20; // Hz + static Alert get(const SubMaster &sm, uint64_t started_frame) { + if (sm.updated("controlsState")) { + const cereal::ControlsState::Reader &cs = sm["controlsState"].getControlsState(); + return {cs.getAlertText1().cStr(), cs.getAlertText2().cStr(), + cs.getAlertType().cStr(), cs.getAlertSize(), + cs.getAlertSound()}; + } else if ((sm.frame - started_frame) > 5 * UI_FREQ) { + const int CONTROLS_TIMEOUT = 5; + // Handle controls timeout + if (sm.rcv_frame("controlsState") < started_frame) { + // car is started, but controlsState hasn't been seen at all + return {"openpilot Unavailable", "Waiting for controls to start", + "controlsWaiting", cereal::ControlsState::AlertSize::MID, + AudibleAlert::NONE}; + } else if ((nanos_since_boot() - sm.rcv_time("controlsState")) / 1e9 > CONTROLS_TIMEOUT) { + // car is started, but controls is lagging or died + return {"TAKE CONTROL IMMEDIATELY", "Controls Unresponsive", + "controlsUnresponsive", cereal::ControlsState::AlertSize::FULL, + AudibleAlert::CHIME_WARNING_REPEAT}; + } + } + return {}; + } +}; typedef enum UIStatus { STATUS_DISENGAGED,