diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore index 1f8ec242b8..5682fe1190 100644 --- a/.devcontainer/.gitignore +++ b/.devcontainer/.gitignore @@ -1 +1,3 @@ -.Xauthority \ No newline at end of file +.Xauthority +.env +.host/ \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e72704bfd0..a24c524047 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,15 @@ FROM ghcr.io/commaai/openpilot-base:latest -# remove gitconfig if exists, since its gonna be replaced by host one -RUN rm -f /root/.gitconfig -RUN apt update && apt install -y vim net-tools usbutils htop ripgrep +RUN apt update && apt install -y vim net-tools usbutils htop ripgrep tmux wget mesa-utils xvfb libxtst6 libxv1 libglu1-mesa libegl1-mesa RUN pip install ipython jupyter jupyterlab + +RUN cd /tmp && \ + ARCH=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && \ + curl -L -o virtualgl.deb "https://downloads.sourceforge.net/project/virtualgl/3.1/virtualgl_3.1_$ARCH.deb" && \ + dpkg -i virtualgl.deb + +USER batman + +RUN cd $HOME && \ + curl -O https://raw.githubusercontent.com/commaai/agnos-builder/master/userspace/home/.tmux.conf && \ + curl -O https://raw.githubusercontent.com/commaai/agnos-builder/master/userspace/home/.vimrc diff --git a/.devcontainer/container_post_create.sh b/.devcontainer/container_post_create.sh new file mode 100755 index 0000000000..dee1c61101 --- /dev/null +++ b/.devcontainer/container_post_create.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +TARGET_USER=batman +source .devcontainer/.host/.env + +# override display flag for mac hosts +if [[ $HOST_OS == darwin ]]; then + echo "Setting up DISPLAY override for macOS..." + cat <> /home/$TARGET_USER/.bashrc +source .devcontainer/.host/.env +if [ -n "\$HOST_DISPLAY" ]; then + DISPLAY_NUM=\$(echo "\$HOST_DISPLAY" | awk -F: '{print \$NF}') + export DISPLAY=host.docker.internal:\$DISPLAY_NUM +fi +EOF +fi + +# setup virtualgl for mac hosts +if [[ $HOST_OS == darwin ]]; then + echo "Setting up virtualgl for macOS..." + cat <> /home/$TARGET_USER/.bashrc +if [ -n "\$HOST_DISPLAY" ]; then + export VGL_PORT=10000 + export VGL_CLIENT=host.docker.internal + export VGL_COMPRESS=rgb + export VGL_DISPLAY=:99 + export VGL_FPS=60 + # prevent vglrun from running exec + alias exec=:; source vglrun :; unalias exec +fi +EOF +fi + +# These lines are temporary, to remain backwards compatible with old devcontainers +# that were running as root and therefore had their caches written as root +sudo chown -R $TARGET_USER: /tmp/scons_cache +sudo chown -R $TARGET_USER: /tmp/comma_download_cache +sudo chown -R $TARGET_USER: /home/batman/.comma diff --git a/.devcontainer/container_post_start.sh b/.devcontainer/container_post_start.sh new file mode 100755 index 0000000000..1521b9c3fb --- /dev/null +++ b/.devcontainer/container_post_start.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +source .devcontainer/.host/.env + +# setup safe directories for submodules +SUBMODULE_DIRS=$(git config --file .gitmodules --get-regexp path | awk '{ print $2 }') +for DIR in $SUBMODULE_DIRS; do + git config --global --add safe.directory "$PWD/$DIR" +done + +# virtual display for virtualgl +if [[ "$HOST_OS" == "darwin" ]] && [[ -n "$HOST_DISPLAY" ]]; then + echo "Starting virtual display at :99 ..." + tmux new-session -d -s fakedisplay Xvfb :99 -screen 0 1920x1080x24 +fi diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json old mode 100755 new mode 100644 index 2dfe46b4f0..a0ff582e68 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,23 +3,37 @@ "build": { "dockerfile": "Dockerfile" }, - "postCreateCommand": "bash -c 'if [[ $DISPLAY == *xquartz* ]]; then echo \"export DISPLAY=host.docker.internal:0\" >> /root/.bashrc; fi'", - "postStartCommand": "git config --file .gitmodules --get-regexp path | awk '{ print $2 }' | xargs -I{} git config --global --add safe.directory \"$PWD/{}\"", - "initializeCommand": ".devcontainer/setup_host.sh", + "postCreateCommand": ".devcontainer/container_post_create.sh", + "postStartCommand": ".devcontainer/container_post_start.sh", + "initializeCommand": [".devcontainer/host_setup"], "privileged": true, "containerEnv": { "DISPLAY": "${localEnv:DISPLAY}", "PYTHONPATH": "${containerWorkspaceFolder}", + "TERM": "xterm-256color", + "CARLA_HOST": "host.docker.internal", "force_color_prompt": "1" }, "runArgs": [ "--volume=/tmp/.X11-unix:/tmp/.X11-unix", - "--volume=${localWorkspaceFolder}/.devcontainer/.Xauthority:/root/.Xauthority", - "--volume=${localEnv:HOME}/.comma:/root/.comma", + "--volume=${localWorkspaceFolder}/.devcontainer/.host/.Xauthority:/home/batman/.Xauthority", + "--volume=${localEnv:HOME}/.comma:/home/batman/.comma", "--volume=/tmp/comma_download_cache:/tmp/comma_download_cache", "--volume=/tmp/devcontainer_scons_cache:/tmp/scons_cache", - "--shm-size=1G" + "--shm-size=1G", + "--add-host=host.docker.internal:host-gateway", // required to use host.docker.internal on linux + "--publish=0.0.0.0:8070-8079:8070-8079" // body ZMQ services ], + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": false, + "installOhMyZsh": false, + "upgradePackages": false, + "username": "batman" + } + }, + "containerUser": "batman", + "remoteUser": "batman", "customizations": { "vscode": { "extensions": [ diff --git a/.devcontainer/host_setup b/.devcontainer/host_setup new file mode 100755 index 0000000000..8ff81ebe42 --- /dev/null +++ b/.devcontainer/host_setup @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# pull base image +if [[ -z $USE_LOCAL_IMAGE ]]; then + echo "Updating openpilot_base image if needed..." + docker pull ghcr.io/commaai/openpilot-base:latest +fi + +# setup .host dir +mkdir -p .devcontainer/.host + +# setup links to Xauthority +XAUTHORITY_LINK=".devcontainer/.host/.Xauthority" +rm -f $XAUTHORITY_LINK +if [[ -z $XAUTHORITY ]]; then + echo "XAUTHORITY not set. Fallback to ~/.Xauthority ..." + if ! [[ -f $HOME/.Xauthority ]]; then + echo "~/.XAuthority file does not exist. GUI tools may not work properly." + touch $XAUTHORITY_LINK # dummy file to satisfy container volume mount + else + ln -sf $HOME/.Xauthority $XAUTHORITY_LINK + fi +else + ln -sf $XAUTHORITY $XAUTHORITY_LINK +fi + +# setup host env file +HOST_INFO_FILE=".devcontainer/.host/.env" +SYSTEM=$(uname -s | tr '[:upper:]' '[:lower:]') +echo "HOST_OS=\"$SYSTEM\"" > $HOST_INFO_FILE +echo "HOST_DISPLAY=\"$DISPLAY\"" >> $HOST_INFO_FILE + +# run virtualgl if macos +if [[ $SYSTEM == "darwin" ]]; then + echo + if [[ -f /opt/VirtualGL/bin/vglclient ]]; then + echo "Starting VirtualGL client at port 10000..." + VGL_LOG_FILE=".devcontainer/.host/.vgl/vglclient.log" + mkdir -p "$(dirname $VGL_LOG_FILE)" + /opt/VirtualGL/bin/vglclient -l "$VGL_LOG_FILE" -display "$DISPLAY" -port 10000 -detach + else + echo "VirtualGL not found. GUI tools may not work properly. Some GUI tools require OpenGL to work properly. To use them with XQuartz on mac, VirtualGL needs to be installed. To install it run:" + echo + echo " brew install --cask virtualgl" + echo + fi +fi diff --git a/.devcontainer/host_setup.cmd b/.devcontainer/host_setup.cmd new file mode 100644 index 0000000000..885bd27f25 --- /dev/null +++ b/.devcontainer/host_setup.cmd @@ -0,0 +1,10 @@ +:: pull base image +IF NOT DEFINED USE_LOCAL_IMAGE ^ +echo "Updating openpilot_base image if needed..." && ^ +docker pull ghcr.io/commaai/openpilot-base:latest + +:: setup .host dir +mkdir .devcontainer\.host + +:: setup host env file +echo "" > .devcontainer\.host\.env \ No newline at end of file diff --git a/.devcontainer/setup_host.sh b/.devcontainer/setup_host.sh deleted file mode 100755 index 5c3c5e900c..0000000000 --- a/.devcontainer/setup_host.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# setup links to Xauthority -XAUTHORITY_LINK=".devcontainer/.Xauthority" -rm -f $XAUTHORITY_LINK -if [[ -z $XAUTHORITY ]]; then - echo "XAUTHORITY not set. Fallback to ~/.Xauthority ..." - if ! [[ -f $HOME/.Xauthority ]]; then - echo "~/.XAuthority file does not exist. GUI tools may not work properly." - touch $XAUTHORITY_LINK # dummy file to satisfy container volume mount - else - ln -sf $HOME/.Xauthority $XAUTHORITY_LINK - fi -else - ln -sf $XAUTHORITY $XAUTHORITY_LINK -fi diff --git a/.dir-locals.el b/.dir-locals.el deleted file mode 100644 index 53017c9fb2..0000000000 --- a/.dir-locals.el +++ /dev/null @@ -1,3 +0,0 @@ -((c++-mode (flycheck-gcc-language-standard . "c++11") - (flycheck-clang-language-standard . "c++11") - )) diff --git a/.dockerignore b/.dockerignore index 1f261adf30..cfc34e5848 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,10 +28,8 @@ chffr/app2 chffr/backend/env selfdrive/nav selfdrive/baseui -chffr/lib/vidindex/vidindex selfdrive/test/simulator2 **/cache_data -xx/chffr/lib/vidindex/vidindex xx/plus xx/community xx/projects diff --git a/.gitattributes b/.gitattributes index 2d1df43fd3..d00208ba84 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,18 @@ +* text=auto + *.dlc filter=lfs diff=lfs merge=lfs -text *.onnx filter=lfs diff=lfs merge=lfs -text selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text +selfdrive/assets/fonts/*.ttf filter=lfs diff=lfs merge=lfs -text +selfdrive/assets/training/*.png filter=lfs diff=lfs merge=lfs -text +system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text +selfdrive/ui/qt/spinner_larch64 filter=lfs diff=lfs merge=lfs -text +selfdrive/ui/qt/text_larch64 filter=lfs diff=lfs merge=lfs -text +third_party/**/*.a filter=lfs diff=lfs merge=lfs -text +third_party/**/*.so filter=lfs diff=lfs merge=lfs -text +third_party/**/*.dylib filter=lfs diff=lfs merge=lfs -text +third_party/acados/*/t_renderer filter=lfs diff=lfs merge=lfs -text +third_party/bootstrap/bootstrap-icons.svg filter=lfs diff=lfs merge=lfs -text +third_party/qt5/larch64/bin/lrelease filter=lfs diff=lfs merge=lfs -text +third_party/qt5/larch64/bin/lupdate filter=lfs diff=lfs merge=lfs -text +third_party/catch2/include/catch2/catch.hpp filter=lfs diff=lfs merge=lfs -text diff --git a/.github/labeler.yaml b/.github/labeler.yaml new file mode 100644 index 0000000000..d4a17ab78c --- /dev/null +++ b/.github/labeler.yaml @@ -0,0 +1,65 @@ +CI / testing: + - all: + - changed-files: ['.github/**', '**/test_*', 'Jenkinsfile'] + +car: + - all: + - changed-files: ['selfdrive/car/**'] + +body: + - all: + - changed-files: ['selfdrive/car/body/*'] +chrysler: + - all: + - changed-files: ['selfdrive/car/chrysler/*'] +ford: + - all: + - changed-files: ['selfdrive/car/ford/*'] +gm: + - all: + - changed-files: ['selfdrive/car/gm/*'] +honda: + - all: + - changed-files: ['selfdrive/car/honda/*'] +hyundai: + - all: + - changed-files: ['selfdrive/car/hyundai/*'] +mazda: + - all: + - changed-files: ['selfdrive/car/mazda/*'] +nissan: + - all: + - changed-files: ['selfdrive/car/nissan/*'] +subaru: + - all: + - changed-files: ['selfdrive/car/subaru/*'] +tesla: + - all: + - changed-files: ['selfdrive/car/tesla/*'] +toyota: + - all: + - changed-files: ['selfdrive/car/toyota/*'] +volkswagen: + - all: + - changed-files: ['selfdrive/car/volkswagen/*'] + +simulation: + - all: + - changed-files: ['tools/sim/**'] +ui: + - all: + - changed-files: ['selfdrive/ui/**'] +tools: + - all: + - changed-files: ['tools/**'] + +multilanguage: + - all: + - changed-files: ['selfdrive/ui/translations/**'] + +research: + - all: + - changed-files: [ + 'selfdrive/modeld/models/**', + 'selfdrive/test/process_replay/model_replay_ref_commit', + ] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index efa947a91a..6fbccfbdbb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,15 @@ + + "; - applyFormatting(fmt); - return *this; - } - - void XmlWriter::writeStylesheetRef( std::string const& url ) { - m_os << "\n"; - } - - XmlWriter& XmlWriter::writeBlankLine() { - ensureTagClosed(); - m_os << '\n'; - return *this; - } - - void XmlWriter::ensureTagClosed() { - if( m_tagIsOpen ) { - m_os << '>' << std::flush; - newlineIfNecessary(); - m_tagIsOpen = false; - } - } - - void XmlWriter::applyFormatting(XmlFormatting fmt) { - m_needsNewline = shouldNewline(fmt); - } - - void XmlWriter::writeDeclaration() { - m_os << "\n"; - } - - void XmlWriter::newlineIfNecessary() { - if( m_needsNewline ) { - m_os << std::endl; - m_needsNewline = false; - } - } -} -// end catch_xmlwriter.cpp -// start catch_reporter_bases.cpp - -#include -#include -#include -#include -#include - -namespace Catch { - void prepareExpandedExpression(AssertionResult& result) { - result.getExpandedExpression(); - } - - // Because formatting using c++ streams is stateful, drop down to C is required - // Alternatively we could use stringstream, but its performance is... not good. - std::string getFormattedDuration( double duration ) { - // Max exponent + 1 is required to represent the whole part - // + 1 for decimal point - // + 3 for the 3 decimal places - // + 1 for null terminator - const std::size_t maxDoubleSize = DBL_MAX_10_EXP + 1 + 1 + 3 + 1; - char buffer[maxDoubleSize]; - - // Save previous errno, to prevent sprintf from overwriting it - ErrnoGuard guard; -#ifdef _MSC_VER - sprintf_s(buffer, "%.3f", duration); -#else - std::sprintf(buffer, "%.3f", duration); -#endif - return std::string(buffer); - } - - bool shouldShowDuration( IConfig const& config, double duration ) { - if ( config.showDurations() == ShowDurations::Always ) { - return true; - } - if ( config.showDurations() == ShowDurations::Never ) { - return false; - } - const double min = config.minDuration(); - return min >= 0 && duration >= min; - } - - std::string serializeFilters( std::vector const& container ) { - ReusableStringStream oss; - bool first = true; - for (auto&& filter : container) - { - if (!first) - oss << ' '; - else - first = false; - - oss << filter; - } - return oss.str(); - } - - TestEventListenerBase::TestEventListenerBase(ReporterConfig const & _config) - :StreamingReporterBase(_config) {} - - std::set TestEventListenerBase::getSupportedVerbosities() { - return { Verbosity::Quiet, Verbosity::Normal, Verbosity::High }; - } - - void TestEventListenerBase::assertionStarting(AssertionInfo const &) {} - - bool TestEventListenerBase::assertionEnded(AssertionStats const &) { - return false; - } - -} // end namespace Catch -// end catch_reporter_bases.cpp -// start catch_reporter_compact.cpp - -namespace { - -#ifdef CATCH_PLATFORM_MAC - const char* failedString() { return "FAILED"; } - const char* passedString() { return "PASSED"; } -#else - const char* failedString() { return "failed"; } - const char* passedString() { return "passed"; } -#endif - - // Colour::LightGrey - Catch::Colour::Code dimColour() { return Catch::Colour::FileName; } - - std::string bothOrAll( std::size_t count ) { - return count == 1 ? std::string() : - count == 2 ? "both " : "all " ; - } - -} // anon namespace - -namespace Catch { -namespace { -// Colour, message variants: -// - white: No tests ran. -// - red: Failed [both/all] N test cases, failed [both/all] M assertions. -// - white: Passed [both/all] N test cases (no assertions). -// - red: Failed N tests cases, failed M assertions. -// - green: Passed [both/all] N tests cases with M assertions. -void printTotals(std::ostream& out, const Totals& totals) { - if (totals.testCases.total() == 0) { - out << "No tests ran."; - } else if (totals.testCases.failed == totals.testCases.total()) { - Colour colour(Colour::ResultError); - const std::string qualify_assertions_failed = - totals.assertions.failed == totals.assertions.total() ? - bothOrAll(totals.assertions.failed) : std::string(); - out << - "Failed " << bothOrAll(totals.testCases.failed) - << pluralise(totals.testCases.failed, "test case") << ", " - "failed " << qualify_assertions_failed << - pluralise(totals.assertions.failed, "assertion") << '.'; - } else if (totals.assertions.total() == 0) { - out << - "Passed " << bothOrAll(totals.testCases.total()) - << pluralise(totals.testCases.total(), "test case") - << " (no assertions)."; - } else if (totals.assertions.failed) { - Colour colour(Colour::ResultError); - out << - "Failed " << pluralise(totals.testCases.failed, "test case") << ", " - "failed " << pluralise(totals.assertions.failed, "assertion") << '.'; - } else { - Colour colour(Colour::ResultSuccess); - out << - "Passed " << bothOrAll(totals.testCases.passed) - << pluralise(totals.testCases.passed, "test case") << - " with " << pluralise(totals.assertions.passed, "assertion") << '.'; - } -} - -// Implementation of CompactReporter formatting -class AssertionPrinter { -public: - AssertionPrinter& operator= (AssertionPrinter const&) = delete; - AssertionPrinter(AssertionPrinter const&) = delete; - AssertionPrinter(std::ostream& _stream, AssertionStats const& _stats, bool _printInfoMessages) - : stream(_stream) - , result(_stats.assertionResult) - , messages(_stats.infoMessages) - , itMessage(_stats.infoMessages.begin()) - , printInfoMessages(_printInfoMessages) {} - - void print() { - printSourceInfo(); - - itMessage = messages.begin(); - - switch (result.getResultType()) { - case ResultWas::Ok: - printResultType(Colour::ResultSuccess, passedString()); - printOriginalExpression(); - printReconstructedExpression(); - if (!result.hasExpression()) - printRemainingMessages(Colour::None); - else - printRemainingMessages(); - break; - case ResultWas::ExpressionFailed: - if (result.isOk()) - printResultType(Colour::ResultSuccess, failedString() + std::string(" - but was ok")); - else - printResultType(Colour::Error, failedString()); - printOriginalExpression(); - printReconstructedExpression(); - printRemainingMessages(); - break; - case ResultWas::ThrewException: - printResultType(Colour::Error, failedString()); - printIssue("unexpected exception with message:"); - printMessage(); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::FatalErrorCondition: - printResultType(Colour::Error, failedString()); - printIssue("fatal error condition with message:"); - printMessage(); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::DidntThrowException: - printResultType(Colour::Error, failedString()); - printIssue("expected exception, got none"); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::Info: - printResultType(Colour::None, "info"); - printMessage(); - printRemainingMessages(); - break; - case ResultWas::Warning: - printResultType(Colour::None, "warning"); - printMessage(); - printRemainingMessages(); - break; - case ResultWas::ExplicitFailure: - printResultType(Colour::Error, failedString()); - printIssue("explicitly"); - printRemainingMessages(Colour::None); - break; - // These cases are here to prevent compiler warnings - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - printResultType(Colour::Error, "** internal error **"); - break; - } - } - -private: - void printSourceInfo() const { - Colour colourGuard(Colour::FileName); - stream << result.getSourceInfo() << ':'; - } - - void printResultType(Colour::Code colour, std::string const& passOrFail) const { - if (!passOrFail.empty()) { - { - Colour colourGuard(colour); - stream << ' ' << passOrFail; - } - stream << ':'; - } - } - - void printIssue(std::string const& issue) const { - stream << ' ' << issue; - } - - void printExpressionWas() { - if (result.hasExpression()) { - stream << ';'; - { - Colour colour(dimColour()); - stream << " expression was:"; - } - printOriginalExpression(); - } - } - - void printOriginalExpression() const { - if (result.hasExpression()) { - stream << ' ' << result.getExpression(); - } - } - - void printReconstructedExpression() const { - if (result.hasExpandedExpression()) { - { - Colour colour(dimColour()); - stream << " for: "; - } - stream << result.getExpandedExpression(); - } - } - - void printMessage() { - if (itMessage != messages.end()) { - stream << " '" << itMessage->message << '\''; - ++itMessage; - } - } - - void printRemainingMessages(Colour::Code colour = dimColour()) { - if (itMessage == messages.end()) - return; - - const auto itEnd = messages.cend(); - const auto N = static_cast(std::distance(itMessage, itEnd)); - - { - Colour colourGuard(colour); - stream << " with " << pluralise(N, "message") << ':'; - } - - while (itMessage != itEnd) { - // If this assertion is a warning ignore any INFO messages - if (printInfoMessages || itMessage->type != ResultWas::Info) { - printMessage(); - if (itMessage != itEnd) { - Colour colourGuard(dimColour()); - stream << " and"; - } - continue; - } - ++itMessage; - } - } - -private: - std::ostream& stream; - AssertionResult const& result; - std::vector messages; - std::vector::const_iterator itMessage; - bool printInfoMessages; -}; - -} // anon namespace - - std::string CompactReporter::getDescription() { - return "Reports test results on a single line, suitable for IDEs"; - } - - void CompactReporter::noMatchingTestCases( std::string const& spec ) { - stream << "No test cases matched '" << spec << '\'' << std::endl; - } - - void CompactReporter::assertionStarting( AssertionInfo const& ) {} - - bool CompactReporter::assertionEnded( AssertionStats const& _assertionStats ) { - AssertionResult const& result = _assertionStats.assertionResult; - - bool printInfoMessages = true; - - // Drop out if result was successful and we're not printing those - if( !m_config->includeSuccessfulResults() && result.isOk() ) { - if( result.getResultType() != ResultWas::Warning ) - return false; - printInfoMessages = false; - } - - AssertionPrinter printer( stream, _assertionStats, printInfoMessages ); - printer.print(); - - stream << std::endl; - return true; - } - - void CompactReporter::sectionEnded(SectionStats const& _sectionStats) { - double dur = _sectionStats.durationInSeconds; - if ( shouldShowDuration( *m_config, dur ) ) { - stream << getFormattedDuration( dur ) << " s: " << _sectionStats.sectionInfo.name << std::endl; - } - } - - void CompactReporter::testRunEnded( TestRunStats const& _testRunStats ) { - printTotals( stream, _testRunStats.totals ); - stream << '\n' << std::endl; - StreamingReporterBase::testRunEnded( _testRunStats ); - } - - CompactReporter::~CompactReporter() {} - - CATCH_REGISTER_REPORTER( "compact", CompactReporter ) - -} // end namespace Catch -// end catch_reporter_compact.cpp -// start catch_reporter_console.cpp - -#include -#include - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4061) // Not all labels are EXPLICITLY handled in switch - // Note that 4062 (not all labels are handled and default is missing) is enabled -#endif - -#if defined(__clang__) -# pragma clang diagnostic push -// For simplicity, benchmarking-only helpers are always enabled -# pragma clang diagnostic ignored "-Wunused-function" -#endif - -namespace Catch { - -namespace { - -// Formatter impl for ConsoleReporter -class ConsoleAssertionPrinter { -public: - ConsoleAssertionPrinter& operator= (ConsoleAssertionPrinter const&) = delete; - ConsoleAssertionPrinter(ConsoleAssertionPrinter const&) = delete; - ConsoleAssertionPrinter(std::ostream& _stream, AssertionStats const& _stats, bool _printInfoMessages) - : stream(_stream), - stats(_stats), - result(_stats.assertionResult), - colour(Colour::None), - message(result.getMessage()), - messages(_stats.infoMessages), - printInfoMessages(_printInfoMessages) { - switch (result.getResultType()) { - case ResultWas::Ok: - colour = Colour::Success; - passOrFail = "PASSED"; - //if( result.hasMessage() ) - if (_stats.infoMessages.size() == 1) - messageLabel = "with message"; - if (_stats.infoMessages.size() > 1) - messageLabel = "with messages"; - break; - case ResultWas::ExpressionFailed: - if (result.isOk()) { - colour = Colour::Success; - passOrFail = "FAILED - but was ok"; - } else { - colour = Colour::Error; - passOrFail = "FAILED"; - } - if (_stats.infoMessages.size() == 1) - messageLabel = "with message"; - if (_stats.infoMessages.size() > 1) - messageLabel = "with messages"; - break; - case ResultWas::ThrewException: - colour = Colour::Error; - passOrFail = "FAILED"; - messageLabel = "due to unexpected exception with "; - if (_stats.infoMessages.size() == 1) - messageLabel += "message"; - if (_stats.infoMessages.size() > 1) - messageLabel += "messages"; - break; - case ResultWas::FatalErrorCondition: - colour = Colour::Error; - passOrFail = "FAILED"; - messageLabel = "due to a fatal error condition"; - break; - case ResultWas::DidntThrowException: - colour = Colour::Error; - passOrFail = "FAILED"; - messageLabel = "because no exception was thrown where one was expected"; - break; - case ResultWas::Info: - messageLabel = "info"; - break; - case ResultWas::Warning: - messageLabel = "warning"; - break; - case ResultWas::ExplicitFailure: - passOrFail = "FAILED"; - colour = Colour::Error; - if (_stats.infoMessages.size() == 1) - messageLabel = "explicitly with message"; - if (_stats.infoMessages.size() > 1) - messageLabel = "explicitly with messages"; - break; - // These cases are here to prevent compiler warnings - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - passOrFail = "** internal error **"; - colour = Colour::Error; - break; - } - } - - void print() const { - printSourceInfo(); - if (stats.totals.assertions.total() > 0) { - printResultType(); - printOriginalExpression(); - printReconstructedExpression(); - } else { - stream << '\n'; - } - printMessage(); - } - -private: - void printResultType() const { - if (!passOrFail.empty()) { - Colour colourGuard(colour); - stream << passOrFail << ":\n"; - } - } - void printOriginalExpression() const { - if (result.hasExpression()) { - Colour colourGuard(Colour::OriginalExpression); - stream << " "; - stream << result.getExpressionInMacro(); - stream << '\n'; - } - } - void printReconstructedExpression() const { - if (result.hasExpandedExpression()) { - stream << "with expansion:\n"; - Colour colourGuard(Colour::ReconstructedExpression); - stream << Column(result.getExpandedExpression()).indent(2) << '\n'; - } - } - void printMessage() const { - if (!messageLabel.empty()) - stream << messageLabel << ':' << '\n'; - for (auto const& msg : messages) { - // If this assertion is a warning ignore any INFO messages - if (printInfoMessages || msg.type != ResultWas::Info) - stream << Column(msg.message).indent(2) << '\n'; - } - } - void printSourceInfo() const { - Colour colourGuard(Colour::FileName); - stream << result.getSourceInfo() << ": "; - } - - std::ostream& stream; - AssertionStats const& stats; - AssertionResult const& result; - Colour::Code colour; - std::string passOrFail; - std::string messageLabel; - std::string message; - std::vector messages; - bool printInfoMessages; -}; - -std::size_t makeRatio(std::size_t number, std::size_t total) { - std::size_t ratio = total > 0 ? CATCH_CONFIG_CONSOLE_WIDTH * number / total : 0; - return (ratio == 0 && number > 0) ? 1 : ratio; -} - -std::size_t& findMax(std::size_t& i, std::size_t& j, std::size_t& k) { - if (i > j && i > k) - return i; - else if (j > k) - return j; - else - return k; -} - -struct ColumnInfo { - enum Justification { Left, Right }; - std::string name; - int width; - Justification justification; -}; -struct ColumnBreak {}; -struct RowBreak {}; - -class Duration { - enum class Unit { - Auto, - Nanoseconds, - Microseconds, - Milliseconds, - Seconds, - Minutes - }; - static const uint64_t s_nanosecondsInAMicrosecond = 1000; - static const uint64_t s_nanosecondsInAMillisecond = 1000 * s_nanosecondsInAMicrosecond; - static const uint64_t s_nanosecondsInASecond = 1000 * s_nanosecondsInAMillisecond; - static const uint64_t s_nanosecondsInAMinute = 60 * s_nanosecondsInASecond; - - double m_inNanoseconds; - Unit m_units; - -public: - explicit Duration(double inNanoseconds, Unit units = Unit::Auto) - : m_inNanoseconds(inNanoseconds), - m_units(units) { - if (m_units == Unit::Auto) { - if (m_inNanoseconds < s_nanosecondsInAMicrosecond) - m_units = Unit::Nanoseconds; - else if (m_inNanoseconds < s_nanosecondsInAMillisecond) - m_units = Unit::Microseconds; - else if (m_inNanoseconds < s_nanosecondsInASecond) - m_units = Unit::Milliseconds; - else if (m_inNanoseconds < s_nanosecondsInAMinute) - m_units = Unit::Seconds; - else - m_units = Unit::Minutes; - } - - } - - auto value() const -> double { - switch (m_units) { - case Unit::Microseconds: - return m_inNanoseconds / static_cast(s_nanosecondsInAMicrosecond); - case Unit::Milliseconds: - return m_inNanoseconds / static_cast(s_nanosecondsInAMillisecond); - case Unit::Seconds: - return m_inNanoseconds / static_cast(s_nanosecondsInASecond); - case Unit::Minutes: - return m_inNanoseconds / static_cast(s_nanosecondsInAMinute); - default: - return m_inNanoseconds; - } - } - auto unitsAsString() const -> std::string { - switch (m_units) { - case Unit::Nanoseconds: - return "ns"; - case Unit::Microseconds: - return "us"; - case Unit::Milliseconds: - return "ms"; - case Unit::Seconds: - return "s"; - case Unit::Minutes: - return "m"; - default: - return "** internal error **"; - } - - } - friend auto operator << (std::ostream& os, Duration const& duration) -> std::ostream& { - return os << duration.value() << ' ' << duration.unitsAsString(); - } -}; -} // end anon namespace - -class TablePrinter { - std::ostream& m_os; - std::vector m_columnInfos; - std::ostringstream m_oss; - int m_currentColumn = -1; - bool m_isOpen = false; - -public: - TablePrinter( std::ostream& os, std::vector columnInfos ) - : m_os( os ), - m_columnInfos( std::move( columnInfos ) ) {} - - auto columnInfos() const -> std::vector const& { - return m_columnInfos; - } - - void open() { - if (!m_isOpen) { - m_isOpen = true; - *this << RowBreak(); - - Columns headerCols; - Spacer spacer(2); - for (auto const& info : m_columnInfos) { - headerCols += Column(info.name).width(static_cast(info.width - 2)); - headerCols += spacer; - } - m_os << headerCols << '\n'; - - m_os << Catch::getLineOfChars<'-'>() << '\n'; - } - } - void close() { - if (m_isOpen) { - *this << RowBreak(); - m_os << std::endl; - m_isOpen = false; - } - } - - template - friend TablePrinter& operator << (TablePrinter& tp, T const& value) { - tp.m_oss << value; - return tp; - } - - friend TablePrinter& operator << (TablePrinter& tp, ColumnBreak) { - auto colStr = tp.m_oss.str(); - const auto strSize = colStr.size(); - tp.m_oss.str(""); - tp.open(); - if (tp.m_currentColumn == static_cast(tp.m_columnInfos.size() - 1)) { - tp.m_currentColumn = -1; - tp.m_os << '\n'; - } - tp.m_currentColumn++; - - auto colInfo = tp.m_columnInfos[tp.m_currentColumn]; - auto padding = (strSize + 1 < static_cast(colInfo.width)) - ? std::string(colInfo.width - (strSize + 1), ' ') - : std::string(); - if (colInfo.justification == ColumnInfo::Left) - tp.m_os << colStr << padding << ' '; - else - tp.m_os << padding << colStr << ' '; - return tp; - } - - friend TablePrinter& operator << (TablePrinter& tp, RowBreak) { - if (tp.m_currentColumn > 0) { - tp.m_os << '\n'; - tp.m_currentColumn = -1; - } - return tp; - } -}; - -ConsoleReporter::ConsoleReporter(ReporterConfig const& config) - : StreamingReporterBase(config), - m_tablePrinter(new TablePrinter(config.stream(), - [&config]() -> std::vector { - if (config.fullConfig()->benchmarkNoAnalysis()) - { - return{ - { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left }, - { " samples", 14, ColumnInfo::Right }, - { " iterations", 14, ColumnInfo::Right }, - { " mean", 14, ColumnInfo::Right } - }; - } - else - { - return{ - { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left }, - { "samples mean std dev", 14, ColumnInfo::Right }, - { "iterations low mean low std dev", 14, ColumnInfo::Right }, - { "estimated high mean high std dev", 14, ColumnInfo::Right } - }; - } - }())) {} -ConsoleReporter::~ConsoleReporter() = default; - -std::string ConsoleReporter::getDescription() { - return "Reports test results as plain lines of text"; -} - -void ConsoleReporter::noMatchingTestCases(std::string const& spec) { - stream << "No test cases matched '" << spec << '\'' << std::endl; -} - -void ConsoleReporter::reportInvalidArguments(std::string const&arg){ - stream << "Invalid Filter: " << arg << std::endl; -} - -void ConsoleReporter::assertionStarting(AssertionInfo const&) {} - -bool ConsoleReporter::assertionEnded(AssertionStats const& _assertionStats) { - AssertionResult const& result = _assertionStats.assertionResult; - - bool includeResults = m_config->includeSuccessfulResults() || !result.isOk(); - - // Drop out if result was successful but we're not printing them. - if (!includeResults && result.getResultType() != ResultWas::Warning) - return false; - - lazyPrint(); - - ConsoleAssertionPrinter printer(stream, _assertionStats, includeResults); - printer.print(); - stream << std::endl; - return true; -} - -void ConsoleReporter::sectionStarting(SectionInfo const& _sectionInfo) { - m_tablePrinter->close(); - m_headerPrinted = false; - StreamingReporterBase::sectionStarting(_sectionInfo); -} -void ConsoleReporter::sectionEnded(SectionStats const& _sectionStats) { - m_tablePrinter->close(); - if (_sectionStats.missingAssertions) { - lazyPrint(); - Colour colour(Colour::ResultError); - if (m_sectionStack.size() > 1) - stream << "\nNo assertions in section"; - else - stream << "\nNo assertions in test case"; - stream << " '" << _sectionStats.sectionInfo.name << "'\n" << std::endl; - } - double dur = _sectionStats.durationInSeconds; - if (shouldShowDuration(*m_config, dur)) { - stream << getFormattedDuration(dur) << " s: " << _sectionStats.sectionInfo.name << std::endl; - } - if (m_headerPrinted) { - m_headerPrinted = false; - } - StreamingReporterBase::sectionEnded(_sectionStats); -} - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) -void ConsoleReporter::benchmarkPreparing(std::string const& name) { - lazyPrintWithoutClosingBenchmarkTable(); - - auto nameCol = Column(name).width(static_cast(m_tablePrinter->columnInfos()[0].width - 2)); - - bool firstLine = true; - for (auto line : nameCol) { - if (!firstLine) - (*m_tablePrinter) << ColumnBreak() << ColumnBreak() << ColumnBreak(); - else - firstLine = false; - - (*m_tablePrinter) << line << ColumnBreak(); - } -} - -void ConsoleReporter::benchmarkStarting(BenchmarkInfo const& info) { - (*m_tablePrinter) << info.samples << ColumnBreak() - << info.iterations << ColumnBreak(); - if (!m_config->benchmarkNoAnalysis()) - (*m_tablePrinter) << Duration(info.estimatedDuration) << ColumnBreak(); -} -void ConsoleReporter::benchmarkEnded(BenchmarkStats<> const& stats) { - if (m_config->benchmarkNoAnalysis()) - { - (*m_tablePrinter) << Duration(stats.mean.point.count()) << ColumnBreak(); - } - else - { - (*m_tablePrinter) << ColumnBreak() - << Duration(stats.mean.point.count()) << ColumnBreak() - << Duration(stats.mean.lower_bound.count()) << ColumnBreak() - << Duration(stats.mean.upper_bound.count()) << ColumnBreak() << ColumnBreak() - << Duration(stats.standardDeviation.point.count()) << ColumnBreak() - << Duration(stats.standardDeviation.lower_bound.count()) << ColumnBreak() - << Duration(stats.standardDeviation.upper_bound.count()) << ColumnBreak() << ColumnBreak() << ColumnBreak() << ColumnBreak() << ColumnBreak(); - } -} - -void ConsoleReporter::benchmarkFailed(std::string const& error) { - Colour colour(Colour::Red); - (*m_tablePrinter) - << "Benchmark failed (" << error << ')' - << ColumnBreak() << RowBreak(); -} -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - -void ConsoleReporter::testCaseEnded(TestCaseStats const& _testCaseStats) { - m_tablePrinter->close(); - StreamingReporterBase::testCaseEnded(_testCaseStats); - m_headerPrinted = false; -} -void ConsoleReporter::testGroupEnded(TestGroupStats const& _testGroupStats) { - if (currentGroupInfo.used) { - printSummaryDivider(); - stream << "Summary for group '" << _testGroupStats.groupInfo.name << "':\n"; - printTotals(_testGroupStats.totals); - stream << '\n' << std::endl; - } - StreamingReporterBase::testGroupEnded(_testGroupStats); -} -void ConsoleReporter::testRunEnded(TestRunStats const& _testRunStats) { - printTotalsDivider(_testRunStats.totals); - printTotals(_testRunStats.totals); - stream << std::endl; - StreamingReporterBase::testRunEnded(_testRunStats); -} -void ConsoleReporter::testRunStarting(TestRunInfo const& _testInfo) { - StreamingReporterBase::testRunStarting(_testInfo); - printTestFilters(); -} - -void ConsoleReporter::lazyPrint() { - - m_tablePrinter->close(); - lazyPrintWithoutClosingBenchmarkTable(); -} - -void ConsoleReporter::lazyPrintWithoutClosingBenchmarkTable() { - - if (!currentTestRunInfo.used) - lazyPrintRunInfo(); - if (!currentGroupInfo.used) - lazyPrintGroupInfo(); - - if (!m_headerPrinted) { - printTestCaseAndSectionHeader(); - m_headerPrinted = true; - } -} -void ConsoleReporter::lazyPrintRunInfo() { - stream << '\n' << getLineOfChars<'~'>() << '\n'; - Colour colour(Colour::SecondaryText); - stream << currentTestRunInfo->name - << " is a Catch v" << libraryVersion() << " host application.\n" - << "Run with -? for options\n\n"; - - if (m_config->rngSeed() != 0) - stream << "Randomness seeded to: " << m_config->rngSeed() << "\n\n"; - - currentTestRunInfo.used = true; -} -void ConsoleReporter::lazyPrintGroupInfo() { - if (!currentGroupInfo->name.empty() && currentGroupInfo->groupsCounts > 1) { - printClosedHeader("Group: " + currentGroupInfo->name); - currentGroupInfo.used = true; - } -} -void ConsoleReporter::printTestCaseAndSectionHeader() { - assert(!m_sectionStack.empty()); - printOpenHeader(currentTestCaseInfo->name); - - if (m_sectionStack.size() > 1) { - Colour colourGuard(Colour::Headers); - - auto - it = m_sectionStack.begin() + 1, // Skip first section (test case) - itEnd = m_sectionStack.end(); - for (; it != itEnd; ++it) - printHeaderString(it->name, 2); - } - - SourceLineInfo lineInfo = m_sectionStack.back().lineInfo; - - stream << getLineOfChars<'-'>() << '\n'; - Colour colourGuard(Colour::FileName); - stream << lineInfo << '\n'; - stream << getLineOfChars<'.'>() << '\n' << std::endl; -} - -void ConsoleReporter::printClosedHeader(std::string const& _name) { - printOpenHeader(_name); - stream << getLineOfChars<'.'>() << '\n'; -} -void ConsoleReporter::printOpenHeader(std::string const& _name) { - stream << getLineOfChars<'-'>() << '\n'; - { - Colour colourGuard(Colour::Headers); - printHeaderString(_name); - } -} - -// if string has a : in first line will set indent to follow it on -// subsequent lines -void ConsoleReporter::printHeaderString(std::string const& _string, std::size_t indent) { - std::size_t i = _string.find(": "); - if (i != std::string::npos) - i += 2; - else - i = 0; - stream << Column(_string).indent(indent + i).initialIndent(indent) << '\n'; -} - -struct SummaryColumn { - - SummaryColumn( std::string _label, Colour::Code _colour ) - : label( std::move( _label ) ), - colour( _colour ) {} - SummaryColumn addRow( std::size_t count ) { - ReusableStringStream rss; - rss << count; - std::string row = rss.str(); - for (auto& oldRow : rows) { - while (oldRow.size() < row.size()) - oldRow = ' ' + oldRow; - while (oldRow.size() > row.size()) - row = ' ' + row; - } - rows.push_back(row); - return *this; - } - - std::string label; - Colour::Code colour; - std::vector rows; - -}; - -void ConsoleReporter::printTotals( Totals const& totals ) { - if (totals.testCases.total() == 0) { - stream << Colour(Colour::Warning) << "No tests ran\n"; - } else if (totals.assertions.total() > 0 && totals.testCases.allPassed()) { - stream << Colour(Colour::ResultSuccess) << "All tests passed"; - stream << " (" - << pluralise(totals.assertions.passed, "assertion") << " in " - << pluralise(totals.testCases.passed, "test case") << ')' - << '\n'; - } else { - - std::vector columns; - columns.push_back(SummaryColumn("", Colour::None) - .addRow(totals.testCases.total()) - .addRow(totals.assertions.total())); - columns.push_back(SummaryColumn("passed", Colour::Success) - .addRow(totals.testCases.passed) - .addRow(totals.assertions.passed)); - columns.push_back(SummaryColumn("failed", Colour::ResultError) - .addRow(totals.testCases.failed) - .addRow(totals.assertions.failed)); - columns.push_back(SummaryColumn("failed as expected", Colour::ResultExpectedFailure) - .addRow(totals.testCases.failedButOk) - .addRow(totals.assertions.failedButOk)); - - printSummaryRow("test cases", columns, 0); - printSummaryRow("assertions", columns, 1); - } -} -void ConsoleReporter::printSummaryRow(std::string const& label, std::vector const& cols, std::size_t row) { - for (auto col : cols) { - std::string value = col.rows[row]; - if (col.label.empty()) { - stream << label << ": "; - if (value != "0") - stream << value; - else - stream << Colour(Colour::Warning) << "- none -"; - } else if (value != "0") { - stream << Colour(Colour::LightGrey) << " | "; - stream << Colour(col.colour) - << value << ' ' << col.label; - } - } - stream << '\n'; -} - -void ConsoleReporter::printTotalsDivider(Totals const& totals) { - if (totals.testCases.total() > 0) { - std::size_t failedRatio = makeRatio(totals.testCases.failed, totals.testCases.total()); - std::size_t failedButOkRatio = makeRatio(totals.testCases.failedButOk, totals.testCases.total()); - std::size_t passedRatio = makeRatio(totals.testCases.passed, totals.testCases.total()); - while (failedRatio + failedButOkRatio + passedRatio < CATCH_CONFIG_CONSOLE_WIDTH - 1) - findMax(failedRatio, failedButOkRatio, passedRatio)++; - while (failedRatio + failedButOkRatio + passedRatio > CATCH_CONFIG_CONSOLE_WIDTH - 1) - findMax(failedRatio, failedButOkRatio, passedRatio)--; - - stream << Colour(Colour::Error) << std::string(failedRatio, '='); - stream << Colour(Colour::ResultExpectedFailure) << std::string(failedButOkRatio, '='); - if (totals.testCases.allPassed()) - stream << Colour(Colour::ResultSuccess) << std::string(passedRatio, '='); - else - stream << Colour(Colour::Success) << std::string(passedRatio, '='); - } else { - stream << Colour(Colour::Warning) << std::string(CATCH_CONFIG_CONSOLE_WIDTH - 1, '='); - } - stream << '\n'; -} -void ConsoleReporter::printSummaryDivider() { - stream << getLineOfChars<'-'>() << '\n'; -} - -void ConsoleReporter::printTestFilters() { - if (m_config->testSpec().hasFilters()) { - Colour guard(Colour::BrightYellow); - stream << "Filters: " << serializeFilters(m_config->getTestsOrTags()) << '\n'; - } -} - -CATCH_REGISTER_REPORTER("console", ConsoleReporter) - -} // end namespace Catch - -#if defined(_MSC_VER) -#pragma warning(pop) -#endif - -#if defined(__clang__) -# pragma clang diagnostic pop -#endif -// end catch_reporter_console.cpp -// start catch_reporter_junit.cpp - -#include -#include -#include -#include -#include - -namespace Catch { - - namespace { - std::string getCurrentTimestamp() { - // Beware, this is not reentrant because of backward compatibility issues - // Also, UTC only, again because of backward compatibility (%z is C++11) - time_t rawtime; - std::time(&rawtime); - auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); - -#ifdef _MSC_VER - std::tm timeInfo = {}; - gmtime_s(&timeInfo, &rawtime); -#else - std::tm* timeInfo; - timeInfo = std::gmtime(&rawtime); -#endif - - char timeStamp[timeStampSize]; - const char * const fmt = "%Y-%m-%dT%H:%M:%SZ"; - -#ifdef _MSC_VER - std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); -#else - std::strftime(timeStamp, timeStampSize, fmt, timeInfo); -#endif - return std::string(timeStamp, timeStampSize-1); - } - - std::string fileNameTag(const std::vector &tags) { - auto it = std::find_if(begin(tags), - end(tags), - [] (std::string const& tag) {return tag.front() == '#'; }); - if (it != tags.end()) - return it->substr(1); - return std::string(); - } - - // Formats the duration in seconds to 3 decimal places. - // This is done because some genius defined Maven Surefire schema - // in a way that only accepts 3 decimal places, and tools like - // Jenkins use that schema for validation JUnit reporter output. - std::string formatDuration( double seconds ) { - ReusableStringStream rss; - rss << std::fixed << std::setprecision( 3 ) << seconds; - return rss.str(); - } - - } // anonymous namespace - - JunitReporter::JunitReporter( ReporterConfig const& _config ) - : CumulativeReporterBase( _config ), - xml( _config.stream() ) - { - m_reporterPrefs.shouldRedirectStdOut = true; - m_reporterPrefs.shouldReportAllAssertions = true; - } - - JunitReporter::~JunitReporter() {} - - std::string JunitReporter::getDescription() { - return "Reports test results in an XML format that looks like Ant's junitreport target"; - } - - void JunitReporter::noMatchingTestCases( std::string const& /*spec*/ ) {} - - void JunitReporter::testRunStarting( TestRunInfo const& runInfo ) { - CumulativeReporterBase::testRunStarting( runInfo ); - xml.startElement( "testsuites" ); - } - - void JunitReporter::testGroupStarting( GroupInfo const& groupInfo ) { - suiteTimer.start(); - stdOutForSuite.clear(); - stdErrForSuite.clear(); - unexpectedExceptions = 0; - CumulativeReporterBase::testGroupStarting( groupInfo ); - } - - void JunitReporter::testCaseStarting( TestCaseInfo const& testCaseInfo ) { - m_okToFail = testCaseInfo.okToFail(); - } - - bool JunitReporter::assertionEnded( AssertionStats const& assertionStats ) { - if( assertionStats.assertionResult.getResultType() == ResultWas::ThrewException && !m_okToFail ) - unexpectedExceptions++; - return CumulativeReporterBase::assertionEnded( assertionStats ); - } - - void JunitReporter::testCaseEnded( TestCaseStats const& testCaseStats ) { - stdOutForSuite += testCaseStats.stdOut; - stdErrForSuite += testCaseStats.stdErr; - CumulativeReporterBase::testCaseEnded( testCaseStats ); - } - - void JunitReporter::testGroupEnded( TestGroupStats const& testGroupStats ) { - double suiteTime = suiteTimer.getElapsedSeconds(); - CumulativeReporterBase::testGroupEnded( testGroupStats ); - writeGroup( *m_testGroups.back(), suiteTime ); - } - - void JunitReporter::testRunEndedCumulative() { - xml.endElement(); - } - - void JunitReporter::writeGroup( TestGroupNode const& groupNode, double suiteTime ) { - XmlWriter::ScopedElement e = xml.scopedElement( "testsuite" ); - - TestGroupStats const& stats = groupNode.value; - xml.writeAttribute( "name", stats.groupInfo.name ); - xml.writeAttribute( "errors", unexpectedExceptions ); - xml.writeAttribute( "failures", stats.totals.assertions.failed-unexpectedExceptions ); - xml.writeAttribute( "tests", stats.totals.assertions.total() ); - xml.writeAttribute( "hostname", "tbd" ); // !TBD - if( m_config->showDurations() == ShowDurations::Never ) - xml.writeAttribute( "time", "" ); - else - xml.writeAttribute( "time", formatDuration( suiteTime ) ); - xml.writeAttribute( "timestamp", getCurrentTimestamp() ); - - // Write properties if there are any - if (m_config->hasTestFilters() || m_config->rngSeed() != 0) { - auto properties = xml.scopedElement("properties"); - if (m_config->hasTestFilters()) { - xml.scopedElement("property") - .writeAttribute("name", "filters") - .writeAttribute("value", serializeFilters(m_config->getTestsOrTags())); - } - if (m_config->rngSeed() != 0) { - xml.scopedElement("property") - .writeAttribute("name", "random-seed") - .writeAttribute("value", m_config->rngSeed()); - } - } - - // Write test cases - for( auto const& child : groupNode.children ) - writeTestCase( *child ); - - xml.scopedElement( "system-out" ).writeText( trim( stdOutForSuite ), XmlFormatting::Newline ); - xml.scopedElement( "system-err" ).writeText( trim( stdErrForSuite ), XmlFormatting::Newline ); - } - - void JunitReporter::writeTestCase( TestCaseNode const& testCaseNode ) { - TestCaseStats const& stats = testCaseNode.value; - - // All test cases have exactly one section - which represents the - // test case itself. That section may have 0-n nested sections - assert( testCaseNode.children.size() == 1 ); - SectionNode const& rootSection = *testCaseNode.children.front(); - - std::string className = stats.testInfo.className; - - if( className.empty() ) { - className = fileNameTag(stats.testInfo.tags); - if ( className.empty() ) - className = "global"; - } - - if ( !m_config->name().empty() ) - className = m_config->name() + "." + className; - - writeSection( className, "", rootSection, stats.testInfo.okToFail() ); - } - - void JunitReporter::writeSection( std::string const& className, - std::string const& rootName, - SectionNode const& sectionNode, - bool testOkToFail) { - std::string name = trim( sectionNode.stats.sectionInfo.name ); - if( !rootName.empty() ) - name = rootName + '/' + name; - - if( !sectionNode.assertions.empty() || - !sectionNode.stdOut.empty() || - !sectionNode.stdErr.empty() ) { - XmlWriter::ScopedElement e = xml.scopedElement( "testcase" ); - if( className.empty() ) { - xml.writeAttribute( "classname", name ); - xml.writeAttribute( "name", "root" ); - } - else { - xml.writeAttribute( "classname", className ); - xml.writeAttribute( "name", name ); - } - xml.writeAttribute( "time", formatDuration( sectionNode.stats.durationInSeconds ) ); - // This is not ideal, but it should be enough to mimic gtest's - // junit output. - // Ideally the JUnit reporter would also handle `skipTest` - // events and write those out appropriately. - xml.writeAttribute( "status", "run" ); - - if (sectionNode.stats.assertions.failedButOk) { - xml.scopedElement("skipped") - .writeAttribute("message", "TEST_CASE tagged with !mayfail"); - } - - writeAssertions( sectionNode ); - - if( !sectionNode.stdOut.empty() ) - xml.scopedElement( "system-out" ).writeText( trim( sectionNode.stdOut ), XmlFormatting::Newline ); - if( !sectionNode.stdErr.empty() ) - xml.scopedElement( "system-err" ).writeText( trim( sectionNode.stdErr ), XmlFormatting::Newline ); - } - for( auto const& childNode : sectionNode.childSections ) - if( className.empty() ) - writeSection( name, "", *childNode, testOkToFail ); - else - writeSection( className, name, *childNode, testOkToFail ); - } - - void JunitReporter::writeAssertions( SectionNode const& sectionNode ) { - for( auto const& assertion : sectionNode.assertions ) - writeAssertion( assertion ); - } - - void JunitReporter::writeAssertion( AssertionStats const& stats ) { - AssertionResult const& result = stats.assertionResult; - if( !result.isOk() ) { - std::string elementName; - switch( result.getResultType() ) { - case ResultWas::ThrewException: - case ResultWas::FatalErrorCondition: - elementName = "error"; - break; - case ResultWas::ExplicitFailure: - case ResultWas::ExpressionFailed: - case ResultWas::DidntThrowException: - elementName = "failure"; - break; - - // We should never see these here: - case ResultWas::Info: - case ResultWas::Warning: - case ResultWas::Ok: - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - elementName = "internalError"; - break; - } - - XmlWriter::ScopedElement e = xml.scopedElement( elementName ); - - xml.writeAttribute( "message", result.getExpression() ); - xml.writeAttribute( "type", result.getTestMacroName() ); - - ReusableStringStream rss; - if (stats.totals.assertions.total() > 0) { - rss << "FAILED" << ":\n"; - if (result.hasExpression()) { - rss << " "; - rss << result.getExpressionInMacro(); - rss << '\n'; - } - if (result.hasExpandedExpression()) { - rss << "with expansion:\n"; - rss << Column(result.getExpandedExpression()).indent(2) << '\n'; - } - } else { - rss << '\n'; - } - - if( !result.getMessage().empty() ) - rss << result.getMessage() << '\n'; - for( auto const& msg : stats.infoMessages ) - if( msg.type == ResultWas::Info ) - rss << msg.message << '\n'; - - rss << "at " << result.getSourceInfo(); - xml.writeText( rss.str(), XmlFormatting::Newline ); - } - } - - CATCH_REGISTER_REPORTER( "junit", JunitReporter ) - -} // end namespace Catch -// end catch_reporter_junit.cpp -// start catch_reporter_listening.cpp - -#include - -namespace Catch { - - ListeningReporter::ListeningReporter() { - // We will assume that listeners will always want all assertions - m_preferences.shouldReportAllAssertions = true; - } - - void ListeningReporter::addListener( IStreamingReporterPtr&& listener ) { - m_listeners.push_back( std::move( listener ) ); - } - - void ListeningReporter::addReporter(IStreamingReporterPtr&& reporter) { - assert(!m_reporter && "Listening reporter can wrap only 1 real reporter"); - m_reporter = std::move( reporter ); - m_preferences.shouldRedirectStdOut = m_reporter->getPreferences().shouldRedirectStdOut; - } - - ReporterPreferences ListeningReporter::getPreferences() const { - return m_preferences; - } - - std::set ListeningReporter::getSupportedVerbosities() { - return std::set{ }; - } - - void ListeningReporter::noMatchingTestCases( std::string const& spec ) { - for ( auto const& listener : m_listeners ) { - listener->noMatchingTestCases( spec ); - } - m_reporter->noMatchingTestCases( spec ); - } - - void ListeningReporter::reportInvalidArguments(std::string const&arg){ - for ( auto const& listener : m_listeners ) { - listener->reportInvalidArguments( arg ); - } - m_reporter->reportInvalidArguments( arg ); - } - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) - void ListeningReporter::benchmarkPreparing( std::string const& name ) { - for (auto const& listener : m_listeners) { - listener->benchmarkPreparing(name); - } - m_reporter->benchmarkPreparing(name); - } - void ListeningReporter::benchmarkStarting( BenchmarkInfo const& benchmarkInfo ) { - for ( auto const& listener : m_listeners ) { - listener->benchmarkStarting( benchmarkInfo ); - } - m_reporter->benchmarkStarting( benchmarkInfo ); - } - void ListeningReporter::benchmarkEnded( BenchmarkStats<> const& benchmarkStats ) { - for ( auto const& listener : m_listeners ) { - listener->benchmarkEnded( benchmarkStats ); - } - m_reporter->benchmarkEnded( benchmarkStats ); - } - - void ListeningReporter::benchmarkFailed( std::string const& error ) { - for (auto const& listener : m_listeners) { - listener->benchmarkFailed(error); - } - m_reporter->benchmarkFailed(error); - } -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - - void ListeningReporter::testRunStarting( TestRunInfo const& testRunInfo ) { - for ( auto const& listener : m_listeners ) { - listener->testRunStarting( testRunInfo ); - } - m_reporter->testRunStarting( testRunInfo ); - } - - void ListeningReporter::testGroupStarting( GroupInfo const& groupInfo ) { - for ( auto const& listener : m_listeners ) { - listener->testGroupStarting( groupInfo ); - } - m_reporter->testGroupStarting( groupInfo ); - } - - void ListeningReporter::testCaseStarting( TestCaseInfo const& testInfo ) { - for ( auto const& listener : m_listeners ) { - listener->testCaseStarting( testInfo ); - } - m_reporter->testCaseStarting( testInfo ); - } - - void ListeningReporter::sectionStarting( SectionInfo const& sectionInfo ) { - for ( auto const& listener : m_listeners ) { - listener->sectionStarting( sectionInfo ); - } - m_reporter->sectionStarting( sectionInfo ); - } - - void ListeningReporter::assertionStarting( AssertionInfo const& assertionInfo ) { - for ( auto const& listener : m_listeners ) { - listener->assertionStarting( assertionInfo ); - } - m_reporter->assertionStarting( assertionInfo ); - } - - // The return value indicates if the messages buffer should be cleared: - bool ListeningReporter::assertionEnded( AssertionStats const& assertionStats ) { - for( auto const& listener : m_listeners ) { - static_cast( listener->assertionEnded( assertionStats ) ); - } - return m_reporter->assertionEnded( assertionStats ); - } - - void ListeningReporter::sectionEnded( SectionStats const& sectionStats ) { - for ( auto const& listener : m_listeners ) { - listener->sectionEnded( sectionStats ); - } - m_reporter->sectionEnded( sectionStats ); - } - - void ListeningReporter::testCaseEnded( TestCaseStats const& testCaseStats ) { - for ( auto const& listener : m_listeners ) { - listener->testCaseEnded( testCaseStats ); - } - m_reporter->testCaseEnded( testCaseStats ); - } - - void ListeningReporter::testGroupEnded( TestGroupStats const& testGroupStats ) { - for ( auto const& listener : m_listeners ) { - listener->testGroupEnded( testGroupStats ); - } - m_reporter->testGroupEnded( testGroupStats ); - } - - void ListeningReporter::testRunEnded( TestRunStats const& testRunStats ) { - for ( auto const& listener : m_listeners ) { - listener->testRunEnded( testRunStats ); - } - m_reporter->testRunEnded( testRunStats ); - } - - void ListeningReporter::skipTest( TestCaseInfo const& testInfo ) { - for ( auto const& listener : m_listeners ) { - listener->skipTest( testInfo ); - } - m_reporter->skipTest( testInfo ); - } - - bool ListeningReporter::isMulti() const { - return true; - } - -} // end namespace Catch -// end catch_reporter_listening.cpp -// start catch_reporter_xml.cpp - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4061) // Not all labels are EXPLICITLY handled in switch - // Note that 4062 (not all labels are handled - // and default is missing) is enabled -#endif - -namespace Catch { - XmlReporter::XmlReporter( ReporterConfig const& _config ) - : StreamingReporterBase( _config ), - m_xml(_config.stream()) - { - m_reporterPrefs.shouldRedirectStdOut = true; - m_reporterPrefs.shouldReportAllAssertions = true; - } - - XmlReporter::~XmlReporter() = default; - - std::string XmlReporter::getDescription() { - return "Reports test results as an XML document"; - } - - std::string XmlReporter::getStylesheetRef() const { - return std::string(); - } - - void XmlReporter::writeSourceInfo( SourceLineInfo const& sourceInfo ) { - m_xml - .writeAttribute( "filename", sourceInfo.file ) - .writeAttribute( "line", sourceInfo.line ); - } - - void XmlReporter::noMatchingTestCases( std::string const& s ) { - StreamingReporterBase::noMatchingTestCases( s ); - } - - void XmlReporter::testRunStarting( TestRunInfo const& testInfo ) { - StreamingReporterBase::testRunStarting( testInfo ); - std::string stylesheetRef = getStylesheetRef(); - if( !stylesheetRef.empty() ) - m_xml.writeStylesheetRef( stylesheetRef ); - m_xml.startElement( "Catch" ); - if( !m_config->name().empty() ) - m_xml.writeAttribute( "name", m_config->name() ); - if (m_config->testSpec().hasFilters()) - m_xml.writeAttribute( "filters", serializeFilters( m_config->getTestsOrTags() ) ); - if( m_config->rngSeed() != 0 ) - m_xml.scopedElement( "Randomness" ) - .writeAttribute( "seed", m_config->rngSeed() ); - } - - void XmlReporter::testGroupStarting( GroupInfo const& groupInfo ) { - StreamingReporterBase::testGroupStarting( groupInfo ); - m_xml.startElement( "Group" ) - .writeAttribute( "name", groupInfo.name ); - } - - void XmlReporter::testCaseStarting( TestCaseInfo const& testInfo ) { - StreamingReporterBase::testCaseStarting(testInfo); - m_xml.startElement( "TestCase" ) - .writeAttribute( "name", trim( testInfo.name ) ) - .writeAttribute( "description", testInfo.description ) - .writeAttribute( "tags", testInfo.tagsAsString() ); - - writeSourceInfo( testInfo.lineInfo ); - - if ( m_config->showDurations() == ShowDurations::Always ) - m_testCaseTimer.start(); - m_xml.ensureTagClosed(); - } - - void XmlReporter::sectionStarting( SectionInfo const& sectionInfo ) { - StreamingReporterBase::sectionStarting( sectionInfo ); - if( m_sectionDepth++ > 0 ) { - m_xml.startElement( "Section" ) - .writeAttribute( "name", trim( sectionInfo.name ) ); - writeSourceInfo( sectionInfo.lineInfo ); - m_xml.ensureTagClosed(); - } - } - - void XmlReporter::assertionStarting( AssertionInfo const& ) { } - - bool XmlReporter::assertionEnded( AssertionStats const& assertionStats ) { - - AssertionResult const& result = assertionStats.assertionResult; - - bool includeResults = m_config->includeSuccessfulResults() || !result.isOk(); - - if( includeResults || result.getResultType() == ResultWas::Warning ) { - // Print any info messages in tags. - for( auto const& msg : assertionStats.infoMessages ) { - if( msg.type == ResultWas::Info && includeResults ) { - m_xml.scopedElement( "Info" ) - .writeText( msg.message ); - } else if ( msg.type == ResultWas::Warning ) { - m_xml.scopedElement( "Warning" ) - .writeText( msg.message ); - } - } - } - - // Drop out if result was successful but we're not printing them. - if( !includeResults && result.getResultType() != ResultWas::Warning ) - return true; - - // Print the expression if there is one. - if( result.hasExpression() ) { - m_xml.startElement( "Expression" ) - .writeAttribute( "success", result.succeeded() ) - .writeAttribute( "type", result.getTestMacroName() ); - - writeSourceInfo( result.getSourceInfo() ); - - m_xml.scopedElement( "Original" ) - .writeText( result.getExpression() ); - m_xml.scopedElement( "Expanded" ) - .writeText( result.getExpandedExpression() ); - } - - // And... Print a result applicable to each result type. - switch( result.getResultType() ) { - case ResultWas::ThrewException: - m_xml.startElement( "Exception" ); - writeSourceInfo( result.getSourceInfo() ); - m_xml.writeText( result.getMessage() ); - m_xml.endElement(); - break; - case ResultWas::FatalErrorCondition: - m_xml.startElement( "FatalErrorCondition" ); - writeSourceInfo( result.getSourceInfo() ); - m_xml.writeText( result.getMessage() ); - m_xml.endElement(); - break; - case ResultWas::Info: - m_xml.scopedElement( "Info" ) - .writeText( result.getMessage() ); - break; - case ResultWas::Warning: - // Warning will already have been written - break; - case ResultWas::ExplicitFailure: - m_xml.startElement( "Failure" ); - writeSourceInfo( result.getSourceInfo() ); - m_xml.writeText( result.getMessage() ); - m_xml.endElement(); - break; - default: - break; - } - - if( result.hasExpression() ) - m_xml.endElement(); - - return true; - } - - void XmlReporter::sectionEnded( SectionStats const& sectionStats ) { - StreamingReporterBase::sectionEnded( sectionStats ); - if( --m_sectionDepth > 0 ) { - XmlWriter::ScopedElement e = m_xml.scopedElement( "OverallResults" ); - e.writeAttribute( "successes", sectionStats.assertions.passed ); - e.writeAttribute( "failures", sectionStats.assertions.failed ); - e.writeAttribute( "expectedFailures", sectionStats.assertions.failedButOk ); - - if ( m_config->showDurations() == ShowDurations::Always ) - e.writeAttribute( "durationInSeconds", sectionStats.durationInSeconds ); - - m_xml.endElement(); - } - } - - void XmlReporter::testCaseEnded( TestCaseStats const& testCaseStats ) { - StreamingReporterBase::testCaseEnded( testCaseStats ); - XmlWriter::ScopedElement e = m_xml.scopedElement( "OverallResult" ); - e.writeAttribute( "success", testCaseStats.totals.assertions.allOk() ); - - if ( m_config->showDurations() == ShowDurations::Always ) - e.writeAttribute( "durationInSeconds", m_testCaseTimer.getElapsedSeconds() ); - - if( !testCaseStats.stdOut.empty() ) - m_xml.scopedElement( "StdOut" ).writeText( trim( testCaseStats.stdOut ), XmlFormatting::Newline ); - if( !testCaseStats.stdErr.empty() ) - m_xml.scopedElement( "StdErr" ).writeText( trim( testCaseStats.stdErr ), XmlFormatting::Newline ); - - m_xml.endElement(); - } - - void XmlReporter::testGroupEnded( TestGroupStats const& testGroupStats ) { - StreamingReporterBase::testGroupEnded( testGroupStats ); - // TODO: Check testGroupStats.aborting and act accordingly. - m_xml.scopedElement( "OverallResults" ) - .writeAttribute( "successes", testGroupStats.totals.assertions.passed ) - .writeAttribute( "failures", testGroupStats.totals.assertions.failed ) - .writeAttribute( "expectedFailures", testGroupStats.totals.assertions.failedButOk ); - m_xml.scopedElement( "OverallResultsCases") - .writeAttribute( "successes", testGroupStats.totals.testCases.passed ) - .writeAttribute( "failures", testGroupStats.totals.testCases.failed ) - .writeAttribute( "expectedFailures", testGroupStats.totals.testCases.failedButOk ); - m_xml.endElement(); - } - - void XmlReporter::testRunEnded( TestRunStats const& testRunStats ) { - StreamingReporterBase::testRunEnded( testRunStats ); - m_xml.scopedElement( "OverallResults" ) - .writeAttribute( "successes", testRunStats.totals.assertions.passed ) - .writeAttribute( "failures", testRunStats.totals.assertions.failed ) - .writeAttribute( "expectedFailures", testRunStats.totals.assertions.failedButOk ); - m_xml.scopedElement( "OverallResultsCases") - .writeAttribute( "successes", testRunStats.totals.testCases.passed ) - .writeAttribute( "failures", testRunStats.totals.testCases.failed ) - .writeAttribute( "expectedFailures", testRunStats.totals.testCases.failedButOk ); - m_xml.endElement(); - } - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) - void XmlReporter::benchmarkPreparing(std::string const& name) { - m_xml.startElement("BenchmarkResults") - .writeAttribute("name", name); - } - - void XmlReporter::benchmarkStarting(BenchmarkInfo const &info) { - m_xml.writeAttribute("samples", info.samples) - .writeAttribute("resamples", info.resamples) - .writeAttribute("iterations", info.iterations) - .writeAttribute("clockResolution", info.clockResolution) - .writeAttribute("estimatedDuration", info.estimatedDuration) - .writeComment("All values in nano seconds"); - } - - void XmlReporter::benchmarkEnded(BenchmarkStats<> const& benchmarkStats) { - m_xml.startElement("mean") - .writeAttribute("value", benchmarkStats.mean.point.count()) - .writeAttribute("lowerBound", benchmarkStats.mean.lower_bound.count()) - .writeAttribute("upperBound", benchmarkStats.mean.upper_bound.count()) - .writeAttribute("ci", benchmarkStats.mean.confidence_interval); - m_xml.endElement(); - m_xml.startElement("standardDeviation") - .writeAttribute("value", benchmarkStats.standardDeviation.point.count()) - .writeAttribute("lowerBound", benchmarkStats.standardDeviation.lower_bound.count()) - .writeAttribute("upperBound", benchmarkStats.standardDeviation.upper_bound.count()) - .writeAttribute("ci", benchmarkStats.standardDeviation.confidence_interval); - m_xml.endElement(); - m_xml.startElement("outliers") - .writeAttribute("variance", benchmarkStats.outlierVariance) - .writeAttribute("lowMild", benchmarkStats.outliers.low_mild) - .writeAttribute("lowSevere", benchmarkStats.outliers.low_severe) - .writeAttribute("highMild", benchmarkStats.outliers.high_mild) - .writeAttribute("highSevere", benchmarkStats.outliers.high_severe); - m_xml.endElement(); - m_xml.endElement(); - } - - void XmlReporter::benchmarkFailed(std::string const &error) { - m_xml.scopedElement("failed"). - writeAttribute("message", error); - m_xml.endElement(); - } -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - - CATCH_REGISTER_REPORTER( "xml", XmlReporter ) - -} // end namespace Catch - -#if defined(_MSC_VER) -#pragma warning(pop) -#endif -// end catch_reporter_xml.cpp - -namespace Catch { - LeakDetector leakDetector; -} - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -// end catch_impl.hpp -#endif - -#ifdef CATCH_CONFIG_MAIN -// start catch_default_main.hpp - -#ifndef __OBJC__ - -#if defined(CATCH_CONFIG_WCHAR) && defined(CATCH_PLATFORM_WINDOWS) && defined(_UNICODE) && !defined(DO_NOT_USE_WMAIN) -// Standard C/C++ Win32 Unicode wmain entry point -extern "C" int wmain (int argc, wchar_t * argv[], wchar_t * []) { -#else -// Standard C/C++ main entry point -int main (int argc, char * argv[]) { -#endif - - return Catch::Session().run( argc, argv ); -} - -#else // __OBJC__ - -// Objective-C entry point -int main (int argc, char * const argv[]) { -#if !CATCH_ARC_ENABLED - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; -#endif - - Catch::registerTestMethods(); - int result = Catch::Session().run( argc, (char**)argv ); - -#if !CATCH_ARC_ENABLED - [pool drain]; -#endif - - return result; -} - -#endif // __OBJC__ - -// end catch_default_main.hpp -#endif - -#if !defined(CATCH_CONFIG_IMPL_ONLY) - -#ifdef CLARA_CONFIG_MAIN_NOT_DEFINED -# undef CLARA_CONFIG_MAIN -#endif - -#if !defined(CATCH_CONFIG_DISABLE) -////// -// If this config identifier is defined then all CATCH macros are prefixed with CATCH_ -#ifdef CATCH_CONFIG_PREFIX_ALL - -#define CATCH_REQUIRE( ... ) INTERNAL_CATCH_TEST( "CATCH_REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define CATCH_REQUIRE_FALSE( ... ) INTERNAL_CATCH_TEST( "CATCH_REQUIRE_FALSE", Catch::ResultDisposition::Normal | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) - -#define CATCH_REQUIRE_THROWS( ... ) INTERNAL_CATCH_THROWS( "CATCH_REQUIRE_THROWS", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define CATCH_REQUIRE_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CATCH_REQUIRE_THROWS_AS", exceptionType, Catch::ResultDisposition::Normal, expr ) -#define CATCH_REQUIRE_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "CATCH_REQUIRE_THROWS_WITH", Catch::ResultDisposition::Normal, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "CATCH_REQUIRE_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::Normal, matcher, expr ) -#endif// CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_REQUIRE_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "CATCH_REQUIRE_NOTHROW", Catch::ResultDisposition::Normal, __VA_ARGS__ ) - -#define CATCH_CHECK( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECK_FALSE( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK_FALSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) -#define CATCH_CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CATCH_CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CATCH_CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECK_NOFAIL( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ ) - -#define CATCH_CHECK_THROWS( ... ) INTERNAL_CATCH_THROWS( "CATCH_CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECK_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CATCH_CHECK_THROWS_AS", exceptionType, Catch::ResultDisposition::ContinueOnFailure, expr ) -#define CATCH_CHECK_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "CATCH_CHECK_THROWS_WITH", Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "CATCH_CHECK_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_CHECK_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "CATCH_CHECK_NOTHROW", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "CATCH_CHECK_THAT", matcher, Catch::ResultDisposition::ContinueOnFailure, arg ) - -#define CATCH_REQUIRE_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "CATCH_REQUIRE_THAT", matcher, Catch::ResultDisposition::Normal, arg ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define CATCH_INFO( msg ) INTERNAL_CATCH_INFO( "CATCH_INFO", msg ) -#define CATCH_UNSCOPED_INFO( msg ) INTERNAL_CATCH_UNSCOPED_INFO( "CATCH_UNSCOPED_INFO", msg ) -#define CATCH_WARN( msg ) INTERNAL_CATCH_MSG( "CATCH_WARN", Catch::ResultWas::Warning, Catch::ResultDisposition::ContinueOnFailure, msg ) -#define CATCH_CAPTURE( ... ) INTERNAL_CATCH_CAPTURE( INTERNAL_CATCH_UNIQUE_NAME(capturer), "CATCH_CAPTURE",__VA_ARGS__ ) - -#define CATCH_TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE( __VA_ARGS__ ) -#define CATCH_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_METHOD_AS_TEST_CASE( method, ... ) INTERNAL_CATCH_METHOD_AS_TEST_CASE( method, __VA_ARGS__ ) -#define CATCH_REGISTER_TEST_CASE( Function, ... ) INTERNAL_CATCH_REGISTER_TESTCASE( Function, __VA_ARGS__ ) -#define CATCH_SECTION( ... ) INTERNAL_CATCH_SECTION( __VA_ARGS__ ) -#define CATCH_DYNAMIC_SECTION( ... ) INTERNAL_CATCH_DYNAMIC_SECTION( __VA_ARGS__ ) -#define CATCH_FAIL( ... ) INTERNAL_CATCH_MSG( "CATCH_FAIL", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define CATCH_FAIL_CHECK( ... ) INTERNAL_CATCH_MSG( "CATCH_FAIL_CHECK", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_SUCCEED( ... ) INTERNAL_CATCH_MSG( "CATCH_SUCCEED", Catch::ResultWas::Ok, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) - -#define CATCH_ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE() - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#else -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#endif - -#if !defined(CATCH_CONFIG_RUNTIME_STATIC_REQUIRE) -#define CATCH_STATIC_REQUIRE( ... ) static_assert( __VA_ARGS__ , #__VA_ARGS__ ); CATCH_SUCCEED( #__VA_ARGS__ ) -#define CATCH_STATIC_REQUIRE_FALSE( ... ) static_assert( !(__VA_ARGS__), "!(" #__VA_ARGS__ ")" ); CATCH_SUCCEED( #__VA_ARGS__ ) -#else -#define CATCH_STATIC_REQUIRE( ... ) CATCH_REQUIRE( __VA_ARGS__ ) -#define CATCH_STATIC_REQUIRE_FALSE( ... ) CATCH_REQUIRE_FALSE( __VA_ARGS__ ) -#endif - -// "BDD-style" convenience wrappers -#define CATCH_SCENARIO( ... ) CATCH_TEST_CASE( "Scenario: " __VA_ARGS__ ) -#define CATCH_SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, "Scenario: " __VA_ARGS__ ) -#define CATCH_GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Given: " << desc ) -#define CATCH_AND_GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( "And given: " << desc ) -#define CATCH_WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " When: " << desc ) -#define CATCH_AND_WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And when: " << desc ) -#define CATCH_THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Then: " << desc ) -#define CATCH_AND_THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And: " << desc ) - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) -#define CATCH_BENCHMARK(...) \ - INTERNAL_CATCH_BENCHMARK(INTERNAL_CATCH_UNIQUE_NAME(C_A_T_C_H_B_E_N_C_H_), INTERNAL_CATCH_GET_1_ARG(__VA_ARGS__,,), INTERNAL_CATCH_GET_2_ARG(__VA_ARGS__,,)) -#define CATCH_BENCHMARK_ADVANCED(name) \ - INTERNAL_CATCH_BENCHMARK_ADVANCED(INTERNAL_CATCH_UNIQUE_NAME(C_A_T_C_H_B_E_N_C_H_), name) -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - -// If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not required -#else - -#define REQUIRE( ... ) INTERNAL_CATCH_TEST( "REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define REQUIRE_FALSE( ... ) INTERNAL_CATCH_TEST( "REQUIRE_FALSE", Catch::ResultDisposition::Normal | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) - -#define REQUIRE_THROWS( ... ) INTERNAL_CATCH_THROWS( "REQUIRE_THROWS", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define REQUIRE_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "REQUIRE_THROWS_AS", exceptionType, Catch::ResultDisposition::Normal, expr ) -#define REQUIRE_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "REQUIRE_THROWS_WITH", Catch::ResultDisposition::Normal, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "REQUIRE_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::Normal, matcher, expr ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define REQUIRE_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "REQUIRE_NOTHROW", Catch::ResultDisposition::Normal, __VA_ARGS__ ) - -#define CHECK( ... ) INTERNAL_CATCH_TEST( "CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECK_FALSE( ... ) INTERNAL_CATCH_TEST( "CHECK_FALSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) -#define CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECK_NOFAIL( ... ) INTERNAL_CATCH_TEST( "CHECK_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ ) - -#define CHECK_THROWS( ... ) INTERNAL_CATCH_THROWS( "CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECK_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CHECK_THROWS_AS", exceptionType, Catch::ResultDisposition::ContinueOnFailure, expr ) -#define CHECK_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "CHECK_THROWS_WITH", Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "CHECK_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CHECK_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "CHECK_NOTHROW", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "CHECK_THAT", matcher, Catch::ResultDisposition::ContinueOnFailure, arg ) - -#define REQUIRE_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "REQUIRE_THAT", matcher, Catch::ResultDisposition::Normal, arg ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define INFO( msg ) INTERNAL_CATCH_INFO( "INFO", msg ) -#define UNSCOPED_INFO( msg ) INTERNAL_CATCH_UNSCOPED_INFO( "UNSCOPED_INFO", msg ) -#define WARN( msg ) INTERNAL_CATCH_MSG( "WARN", Catch::ResultWas::Warning, Catch::ResultDisposition::ContinueOnFailure, msg ) -#define CAPTURE( ... ) INTERNAL_CATCH_CAPTURE( INTERNAL_CATCH_UNIQUE_NAME(capturer), "CAPTURE",__VA_ARGS__ ) - -#define TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE( __VA_ARGS__ ) -#define TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define METHOD_AS_TEST_CASE( method, ... ) INTERNAL_CATCH_METHOD_AS_TEST_CASE( method, __VA_ARGS__ ) -#define REGISTER_TEST_CASE( Function, ... ) INTERNAL_CATCH_REGISTER_TESTCASE( Function, __VA_ARGS__ ) -#define SECTION( ... ) INTERNAL_CATCH_SECTION( __VA_ARGS__ ) -#define DYNAMIC_SECTION( ... ) INTERNAL_CATCH_DYNAMIC_SECTION( __VA_ARGS__ ) -#define FAIL( ... ) INTERNAL_CATCH_MSG( "FAIL", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define FAIL_CHECK( ... ) INTERNAL_CATCH_MSG( "FAIL_CHECK", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define SUCCEED( ... ) INTERNAL_CATCH_MSG( "SUCCEED", Catch::ResultWas::Ok, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE() - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#define TEMPLATE_LIST_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE(__VA_ARGS__) -#define TEMPLATE_LIST_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#else -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#define TEMPLATE_LIST_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE( __VA_ARGS__ ) ) -#define TEMPLATE_LIST_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#endif - -#if !defined(CATCH_CONFIG_RUNTIME_STATIC_REQUIRE) -#define STATIC_REQUIRE( ... ) static_assert( __VA_ARGS__, #__VA_ARGS__ ); SUCCEED( #__VA_ARGS__ ) -#define STATIC_REQUIRE_FALSE( ... ) static_assert( !(__VA_ARGS__), "!(" #__VA_ARGS__ ")" ); SUCCEED( "!(" #__VA_ARGS__ ")" ) -#else -#define STATIC_REQUIRE( ... ) REQUIRE( __VA_ARGS__ ) -#define STATIC_REQUIRE_FALSE( ... ) REQUIRE_FALSE( __VA_ARGS__ ) -#endif - -#endif - -#define CATCH_TRANSLATE_EXCEPTION( signature ) INTERNAL_CATCH_TRANSLATE_EXCEPTION( signature ) - -// "BDD-style" convenience wrappers -#define SCENARIO( ... ) TEST_CASE( "Scenario: " __VA_ARGS__ ) -#define SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, "Scenario: " __VA_ARGS__ ) - -#define GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Given: " << desc ) -#define AND_GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( "And given: " << desc ) -#define WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " When: " << desc ) -#define AND_WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And when: " << desc ) -#define THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Then: " << desc ) -#define AND_THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And: " << desc ) - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) -#define BENCHMARK(...) \ - INTERNAL_CATCH_BENCHMARK(INTERNAL_CATCH_UNIQUE_NAME(C_A_T_C_H_B_E_N_C_H_), INTERNAL_CATCH_GET_1_ARG(__VA_ARGS__,,), INTERNAL_CATCH_GET_2_ARG(__VA_ARGS__,,)) -#define BENCHMARK_ADVANCED(name) \ - INTERNAL_CATCH_BENCHMARK_ADVANCED(INTERNAL_CATCH_UNIQUE_NAME(C_A_T_C_H_B_E_N_C_H_), name) -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - -using Catch::Detail::Approx; - -#else // CATCH_CONFIG_DISABLE - -////// -// If this config identifier is defined then all CATCH macros are prefixed with CATCH_ -#ifdef CATCH_CONFIG_PREFIX_ALL - -#define CATCH_REQUIRE( ... ) (void)(0) -#define CATCH_REQUIRE_FALSE( ... ) (void)(0) - -#define CATCH_REQUIRE_THROWS( ... ) (void)(0) -#define CATCH_REQUIRE_THROWS_AS( expr, exceptionType ) (void)(0) -#define CATCH_REQUIRE_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif// CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_REQUIRE_NOTHROW( ... ) (void)(0) - -#define CATCH_CHECK( ... ) (void)(0) -#define CATCH_CHECK_FALSE( ... ) (void)(0) -#define CATCH_CHECKED_IF( ... ) if (__VA_ARGS__) -#define CATCH_CHECKED_ELSE( ... ) if (!(__VA_ARGS__)) -#define CATCH_CHECK_NOFAIL( ... ) (void)(0) - -#define CATCH_CHECK_THROWS( ... ) (void)(0) -#define CATCH_CHECK_THROWS_AS( expr, exceptionType ) (void)(0) -#define CATCH_CHECK_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_CHECK_NOTHROW( ... ) (void)(0) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THAT( arg, matcher ) (void)(0) - -#define CATCH_REQUIRE_THAT( arg, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define CATCH_INFO( msg ) (void)(0) -#define CATCH_UNSCOPED_INFO( msg ) (void)(0) -#define CATCH_WARN( msg ) (void)(0) -#define CATCH_CAPTURE( msg ) (void)(0) - -#define CATCH_TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ )) -#define CATCH_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ )) -#define CATCH_METHOD_AS_TEST_CASE( method, ... ) -#define CATCH_REGISTER_TEST_CASE( Function, ... ) (void)(0) -#define CATCH_SECTION( ... ) -#define CATCH_DYNAMIC_SECTION( ... ) -#define CATCH_FAIL( ... ) (void)(0) -#define CATCH_FAIL_CHECK( ... ) (void)(0) -#define CATCH_SUCCEED( ... ) (void)(0) - -#define CATCH_ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ )) - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#else -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) ) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#endif - -// "BDD-style" convenience wrappers -#define CATCH_SCENARIO( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ )) -#define CATCH_SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_METHOD_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ ), className ) -#define CATCH_GIVEN( desc ) -#define CATCH_AND_GIVEN( desc ) -#define CATCH_WHEN( desc ) -#define CATCH_AND_WHEN( desc ) -#define CATCH_THEN( desc ) -#define CATCH_AND_THEN( desc ) - -#define CATCH_STATIC_REQUIRE( ... ) (void)(0) -#define CATCH_STATIC_REQUIRE_FALSE( ... ) (void)(0) - -// If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not required -#else - -#define REQUIRE( ... ) (void)(0) -#define REQUIRE_FALSE( ... ) (void)(0) - -#define REQUIRE_THROWS( ... ) (void)(0) -#define REQUIRE_THROWS_AS( expr, exceptionType ) (void)(0) -#define REQUIRE_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define REQUIRE_NOTHROW( ... ) (void)(0) - -#define CHECK( ... ) (void)(0) -#define CHECK_FALSE( ... ) (void)(0) -#define CHECKED_IF( ... ) if (__VA_ARGS__) -#define CHECKED_ELSE( ... ) if (!(__VA_ARGS__)) -#define CHECK_NOFAIL( ... ) (void)(0) - -#define CHECK_THROWS( ... ) (void)(0) -#define CHECK_THROWS_AS( expr, exceptionType ) (void)(0) -#define CHECK_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CHECK_NOTHROW( ... ) (void)(0) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THAT( arg, matcher ) (void)(0) - -#define REQUIRE_THAT( arg, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define INFO( msg ) (void)(0) -#define UNSCOPED_INFO( msg ) (void)(0) -#define WARN( msg ) (void)(0) -#define CAPTURE( ... ) (void)(0) - -#define TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ )) -#define TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ )) -#define METHOD_AS_TEST_CASE( method, ... ) -#define REGISTER_TEST_CASE( Function, ... ) (void)(0) -#define SECTION( ... ) -#define DYNAMIC_SECTION( ... ) -#define FAIL( ... ) (void)(0) -#define FAIL_CHECK( ... ) (void)(0) -#define SUCCEED( ... ) (void)(0) -#define ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ )) - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#else -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) ) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) ) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#endif - -#define STATIC_REQUIRE( ... ) (void)(0) -#define STATIC_REQUIRE_FALSE( ... ) (void)(0) - -#endif - -#define CATCH_TRANSLATE_EXCEPTION( signature ) INTERNAL_CATCH_TRANSLATE_EXCEPTION_NO_REG( INTERNAL_CATCH_UNIQUE_NAME( catch_internal_ExceptionTranslator ), signature ) - -// "BDD-style" convenience wrappers -#define SCENARIO( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ ) ) -#define SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_METHOD_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( C_A_T_C_H_T_E_S_T_ ), className ) - -#define GIVEN( desc ) -#define AND_GIVEN( desc ) -#define WHEN( desc ) -#define AND_WHEN( desc ) -#define THEN( desc ) -#define AND_THEN( desc ) - -using Catch::Detail::Approx; - -#endif - -#endif // ! CATCH_CONFIG_IMPL_ONLY - -// start catch_reenable_warnings.h - - -#ifdef __clang__ -# ifdef __ICC // icpc defines the __clang__ macro -# pragma warning(pop) -# else -# pragma clang diagnostic pop -# endif -#elif defined __GNUC__ -# pragma GCC diagnostic pop -#endif - -// end catch_reenable_warnings.h -// end catch.hpp -#endif // TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED - +version https://git-lfs.github.com/spec/v1 +oid sha256:27da57c7a06d09be8dd81fab7246b79e7892b6ae7e4e49ba8631f1d5a955e3fc +size 657276 diff --git a/third_party/catch2/include/catch2/catch_reporter_automake.hpp b/third_party/catch2/include/catch2/catch_reporter_automake.hpp index ba6f67f310..dbebe97516 100644 --- a/third_party/catch2/include/catch2/catch_reporter_automake.hpp +++ b/third_party/catch2/include/catch2/catch_reporter_automake.hpp @@ -1,62 +1,62 @@ -/* - * Created by Justin R. Wilson on 2/19/2017. - * Copyright 2017 Justin R. Wilson. All rights reserved. - * - * Distributed under the Boost Software License, Version 1.0. (See accompanying - * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - */ -#ifndef TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED -#define TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED - -// Don't #include any Catch headers here - we can assume they are already -// included before this header. -// This is not good practice in general but is necessary in this case so this -// file can be distributed as a single header that works with the main -// Catch single header. - -namespace Catch { - - struct AutomakeReporter : StreamingReporterBase { - AutomakeReporter( ReporterConfig const& _config ) - : StreamingReporterBase( _config ) - {} - - ~AutomakeReporter() override; - - static std::string getDescription() { - return "Reports test results in the format of Automake .trs files"; - } - - void assertionStarting( AssertionInfo const& ) override {} - - bool assertionEnded( AssertionStats const& /*_assertionStats*/ ) override { return true; } - - void testCaseEnded( TestCaseStats const& _testCaseStats ) override { - // Possible values to emit are PASS, XFAIL, SKIP, FAIL, XPASS and ERROR. - stream << ":test-result: "; - if (_testCaseStats.totals.assertions.allPassed()) { - stream << "PASS"; - } else if (_testCaseStats.totals.assertions.allOk()) { - stream << "XFAIL"; - } else { - stream << "FAIL"; - } - stream << ' ' << _testCaseStats.testInfo.name << '\n'; - StreamingReporterBase::testCaseEnded( _testCaseStats ); - } - - void skipTest( TestCaseInfo const& testInfo ) override { - stream << ":test-result: SKIP " << testInfo.name << '\n'; - } - - }; - -#ifdef CATCH_IMPL - AutomakeReporter::~AutomakeReporter() {} -#endif - - CATCH_REGISTER_REPORTER( "automake", AutomakeReporter) - -} // end namespace Catch - -#endif // TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED +/* + * Created by Justin R. Wilson on 2/19/2017. + * Copyright 2017 Justin R. Wilson. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED +#define TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED + +// Don't #include any Catch headers here - we can assume they are already +// included before this header. +// This is not good practice in general but is necessary in this case so this +// file can be distributed as a single header that works with the main +// Catch single header. + +namespace Catch { + + struct AutomakeReporter : StreamingReporterBase { + AutomakeReporter( ReporterConfig const& _config ) + : StreamingReporterBase( _config ) + {} + + ~AutomakeReporter() override; + + static std::string getDescription() { + return "Reports test results in the format of Automake .trs files"; + } + + void assertionStarting( AssertionInfo const& ) override {} + + bool assertionEnded( AssertionStats const& /*_assertionStats*/ ) override { return true; } + + void testCaseEnded( TestCaseStats const& _testCaseStats ) override { + // Possible values to emit are PASS, XFAIL, SKIP, FAIL, XPASS and ERROR. + stream << ":test-result: "; + if (_testCaseStats.totals.assertions.allPassed()) { + stream << "PASS"; + } else if (_testCaseStats.totals.assertions.allOk()) { + stream << "XFAIL"; + } else { + stream << "FAIL"; + } + stream << ' ' << _testCaseStats.testInfo.name << '\n'; + StreamingReporterBase::testCaseEnded( _testCaseStats ); + } + + void skipTest( TestCaseInfo const& testInfo ) override { + stream << ":test-result: SKIP " << testInfo.name << '\n'; + } + + }; + +#ifdef CATCH_IMPL + AutomakeReporter::~AutomakeReporter() {} +#endif + + CATCH_REGISTER_REPORTER( "automake", AutomakeReporter) + +} // end namespace Catch + +#endif // TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED diff --git a/third_party/catch2/include/catch2/catch_reporter_sonarqube.hpp b/third_party/catch2/include/catch2/catch_reporter_sonarqube.hpp index 06056f154f..bf7d9299a5 100644 --- a/third_party/catch2/include/catch2/catch_reporter_sonarqube.hpp +++ b/third_party/catch2/include/catch2/catch_reporter_sonarqube.hpp @@ -1,181 +1,181 @@ -/* - * Created by Daniel Garcia on 2018-12-04. - * Copyright Social Point SL. All rights reserved. - * - * Distributed under the Boost Software License, Version 1.0. (See accompanying - * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - */ -#ifndef CATCH_REPORTER_SONARQUBE_HPP_INCLUDED -#define CATCH_REPORTER_SONARQUBE_HPP_INCLUDED - - -// Don't #include any Catch headers here - we can assume they are already -// included before this header. -// This is not good practice in general but is necessary in this case so this -// file can be distributed as a single header that works with the main -// Catch single header. - -#include - -namespace Catch { - - struct SonarQubeReporter : CumulativeReporterBase { - - SonarQubeReporter(ReporterConfig const& config) - : CumulativeReporterBase(config) - , xml(config.stream()) { - m_reporterPrefs.shouldRedirectStdOut = true; - m_reporterPrefs.shouldReportAllAssertions = true; - } - - ~SonarQubeReporter() override; - - static std::string getDescription() { - return "Reports test results in the Generic Test Data SonarQube XML format"; - } - - static std::set getSupportedVerbosities() { - return { Verbosity::Normal }; - } - - void noMatchingTestCases(std::string const& /*spec*/) override {} - - void testRunStarting(TestRunInfo const& testRunInfo) override { - CumulativeReporterBase::testRunStarting(testRunInfo); - xml.startElement("testExecutions"); - xml.writeAttribute("version", "1"); - } - - void testGroupEnded(TestGroupStats const& testGroupStats) override { - CumulativeReporterBase::testGroupEnded(testGroupStats); - writeGroup(*m_testGroups.back()); - } - - void testRunEndedCumulative() override { - xml.endElement(); - } - - void writeGroup(TestGroupNode const& groupNode) { - std::map testsPerFile; - for(auto const& child : groupNode.children) - testsPerFile[child->value.testInfo.lineInfo.file].push_back(child); - - for(auto const& kv : testsPerFile) - writeTestFile(kv.first.c_str(), kv.second); - } - - void writeTestFile(const char* filename, TestGroupNode::ChildNodes const& testCaseNodes) { - XmlWriter::ScopedElement e = xml.scopedElement("file"); - xml.writeAttribute("path", filename); - - for(auto const& child : testCaseNodes) - writeTestCase(*child); - } - - void writeTestCase(TestCaseNode const& testCaseNode) { - // All test cases have exactly one section - which represents the - // test case itself. That section may have 0-n nested sections - assert(testCaseNode.children.size() == 1); - SectionNode const& rootSection = *testCaseNode.children.front(); - writeSection("", rootSection, testCaseNode.value.testInfo.okToFail()); - } - - void writeSection(std::string const& rootName, SectionNode const& sectionNode, bool okToFail) { - std::string name = trim(sectionNode.stats.sectionInfo.name); - if(!rootName.empty()) - name = rootName + '/' + name; - - if(!sectionNode.assertions.empty() || !sectionNode.stdOut.empty() || !sectionNode.stdErr.empty()) { - XmlWriter::ScopedElement e = xml.scopedElement("testCase"); - xml.writeAttribute("name", name); - xml.writeAttribute("duration", static_cast(sectionNode.stats.durationInSeconds * 1000)); - - writeAssertions(sectionNode, okToFail); - } - - for(auto const& childNode : sectionNode.childSections) - writeSection(name, *childNode, okToFail); - } - - void writeAssertions(SectionNode const& sectionNode, bool okToFail) { - for(auto const& assertion : sectionNode.assertions) - writeAssertion( assertion, okToFail); - } - - void writeAssertion(AssertionStats const& stats, bool okToFail) { - AssertionResult const& result = stats.assertionResult; - if(!result.isOk()) { - std::string elementName; - if(okToFail) { - elementName = "skipped"; - } - else { - switch(result.getResultType()) { - case ResultWas::ThrewException: - case ResultWas::FatalErrorCondition: - elementName = "error"; - break; - case ResultWas::ExplicitFailure: - elementName = "failure"; - break; - case ResultWas::ExpressionFailed: - elementName = "failure"; - break; - case ResultWas::DidntThrowException: - elementName = "failure"; - break; - - // We should never see these here: - case ResultWas::Info: - case ResultWas::Warning: - case ResultWas::Ok: - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - elementName = "internalError"; - break; - } - } - - XmlWriter::ScopedElement e = xml.scopedElement(elementName); - - ReusableStringStream messageRss; - messageRss << result.getTestMacroName() << "(" << result.getExpression() << ")"; - xml.writeAttribute("message", messageRss.str()); - - ReusableStringStream textRss; - if (stats.totals.assertions.total() > 0) { - textRss << "FAILED:\n"; - if (result.hasExpression()) { - textRss << "\t" << result.getExpressionInMacro() << "\n"; - } - if (result.hasExpandedExpression()) { - textRss << "with expansion:\n\t" << result.getExpandedExpression() << "\n"; - } - } - - if(!result.getMessage().empty()) - textRss << result.getMessage() << "\n"; - - for(auto const& msg : stats.infoMessages) - if(msg.type == ResultWas::Info) - textRss << msg.message << "\n"; - - textRss << "at " << result.getSourceInfo(); - xml.writeText(textRss.str(), XmlFormatting::Newline); - } - } - - private: - XmlWriter xml; - }; - -#ifdef CATCH_IMPL - SonarQubeReporter::~SonarQubeReporter() {} -#endif - - CATCH_REGISTER_REPORTER( "sonarqube", SonarQubeReporter ) - -} // end namespace Catch - +/* + * Created by Daniel Garcia on 2018-12-04. + * Copyright Social Point SL. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef CATCH_REPORTER_SONARQUBE_HPP_INCLUDED +#define CATCH_REPORTER_SONARQUBE_HPP_INCLUDED + + +// Don't #include any Catch headers here - we can assume they are already +// included before this header. +// This is not good practice in general but is necessary in this case so this +// file can be distributed as a single header that works with the main +// Catch single header. + +#include + +namespace Catch { + + struct SonarQubeReporter : CumulativeReporterBase { + + SonarQubeReporter(ReporterConfig const& config) + : CumulativeReporterBase(config) + , xml(config.stream()) { + m_reporterPrefs.shouldRedirectStdOut = true; + m_reporterPrefs.shouldReportAllAssertions = true; + } + + ~SonarQubeReporter() override; + + static std::string getDescription() { + return "Reports test results in the Generic Test Data SonarQube XML format"; + } + + static std::set getSupportedVerbosities() { + return { Verbosity::Normal }; + } + + void noMatchingTestCases(std::string const& /*spec*/) override {} + + void testRunStarting(TestRunInfo const& testRunInfo) override { + CumulativeReporterBase::testRunStarting(testRunInfo); + xml.startElement("testExecutions"); + xml.writeAttribute("version", "1"); + } + + void testGroupEnded(TestGroupStats const& testGroupStats) override { + CumulativeReporterBase::testGroupEnded(testGroupStats); + writeGroup(*m_testGroups.back()); + } + + void testRunEndedCumulative() override { + xml.endElement(); + } + + void writeGroup(TestGroupNode const& groupNode) { + std::map testsPerFile; + for(auto const& child : groupNode.children) + testsPerFile[child->value.testInfo.lineInfo.file].push_back(child); + + for(auto const& kv : testsPerFile) + writeTestFile(kv.first.c_str(), kv.second); + } + + void writeTestFile(const char* filename, TestGroupNode::ChildNodes const& testCaseNodes) { + XmlWriter::ScopedElement e = xml.scopedElement("file"); + xml.writeAttribute("path", filename); + + for(auto const& child : testCaseNodes) + writeTestCase(*child); + } + + void writeTestCase(TestCaseNode const& testCaseNode) { + // All test cases have exactly one section - which represents the + // test case itself. That section may have 0-n nested sections + assert(testCaseNode.children.size() == 1); + SectionNode const& rootSection = *testCaseNode.children.front(); + writeSection("", rootSection, testCaseNode.value.testInfo.okToFail()); + } + + void writeSection(std::string const& rootName, SectionNode const& sectionNode, bool okToFail) { + std::string name = trim(sectionNode.stats.sectionInfo.name); + if(!rootName.empty()) + name = rootName + '/' + name; + + if(!sectionNode.assertions.empty() || !sectionNode.stdOut.empty() || !sectionNode.stdErr.empty()) { + XmlWriter::ScopedElement e = xml.scopedElement("testCase"); + xml.writeAttribute("name", name); + xml.writeAttribute("duration", static_cast(sectionNode.stats.durationInSeconds * 1000)); + + writeAssertions(sectionNode, okToFail); + } + + for(auto const& childNode : sectionNode.childSections) + writeSection(name, *childNode, okToFail); + } + + void writeAssertions(SectionNode const& sectionNode, bool okToFail) { + for(auto const& assertion : sectionNode.assertions) + writeAssertion( assertion, okToFail); + } + + void writeAssertion(AssertionStats const& stats, bool okToFail) { + AssertionResult const& result = stats.assertionResult; + if(!result.isOk()) { + std::string elementName; + if(okToFail) { + elementName = "skipped"; + } + else { + switch(result.getResultType()) { + case ResultWas::ThrewException: + case ResultWas::FatalErrorCondition: + elementName = "error"; + break; + case ResultWas::ExplicitFailure: + elementName = "failure"; + break; + case ResultWas::ExpressionFailed: + elementName = "failure"; + break; + case ResultWas::DidntThrowException: + elementName = "failure"; + break; + + // We should never see these here: + case ResultWas::Info: + case ResultWas::Warning: + case ResultWas::Ok: + case ResultWas::Unknown: + case ResultWas::FailureBit: + case ResultWas::Exception: + elementName = "internalError"; + break; + } + } + + XmlWriter::ScopedElement e = xml.scopedElement(elementName); + + ReusableStringStream messageRss; + messageRss << result.getTestMacroName() << "(" << result.getExpression() << ")"; + xml.writeAttribute("message", messageRss.str()); + + ReusableStringStream textRss; + if (stats.totals.assertions.total() > 0) { + textRss << "FAILED:\n"; + if (result.hasExpression()) { + textRss << "\t" << result.getExpressionInMacro() << "\n"; + } + if (result.hasExpandedExpression()) { + textRss << "with expansion:\n\t" << result.getExpandedExpression() << "\n"; + } + } + + if(!result.getMessage().empty()) + textRss << result.getMessage() << "\n"; + + for(auto const& msg : stats.infoMessages) + if(msg.type == ResultWas::Info) + textRss << msg.message << "\n"; + + textRss << "at " << result.getSourceInfo(); + xml.writeText(textRss.str(), XmlFormatting::Newline); + } + } + + private: + XmlWriter xml; + }; + +#ifdef CATCH_IMPL + SonarQubeReporter::~SonarQubeReporter() {} +#endif + + CATCH_REGISTER_REPORTER( "sonarqube", SonarQubeReporter ) + +} // end namespace Catch + #endif // CATCH_REPORTER_SONARQUBE_HPP_INCLUDED \ No newline at end of file diff --git a/third_party/catch2/include/catch2/catch_reporter_tap.hpp b/third_party/catch2/include/catch2/catch_reporter_tap.hpp index 3acab5f39d..5ac8524ce7 100644 --- a/third_party/catch2/include/catch2/catch_reporter_tap.hpp +++ b/third_party/catch2/include/catch2/catch_reporter_tap.hpp @@ -1,254 +1,254 @@ -/* - * Created by Colton Wolkins on 2015-08-15. - * Copyright 2015 Martin Moene. All rights reserved. - * - * Distributed under the Boost Software License, Version 1.0. (See accompanying - * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - */ -#ifndef TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED -#define TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED - - -// Don't #include any Catch headers here - we can assume they are already -// included before this header. -// This is not good practice in general but is necessary in this case so this -// file can be distributed as a single header that works with the main -// Catch single header. - -#include - -namespace Catch { - - struct TAPReporter : StreamingReporterBase { - - using StreamingReporterBase::StreamingReporterBase; - - TAPReporter( ReporterConfig const& config ): - StreamingReporterBase( config ) { - m_reporterPrefs.shouldReportAllAssertions = true; - } - - ~TAPReporter() override; - - static std::string getDescription() { - return "Reports test results in TAP format, suitable for test harnesses"; - } - - void noMatchingTestCases( std::string const& spec ) override { - stream << "# No test cases matched '" << spec << "'" << std::endl; - } - - void assertionStarting( AssertionInfo const& ) override {} - - bool assertionEnded( AssertionStats const& _assertionStats ) override { - ++counter; - - stream << "# " << currentTestCaseInfo->name << std::endl; - AssertionPrinter printer( stream, _assertionStats, counter ); - printer.print(); - - stream << std::endl; - return true; - } - - void testRunEnded( TestRunStats const& _testRunStats ) override { - printTotals( _testRunStats.totals ); - stream << "\n" << std::endl; - StreamingReporterBase::testRunEnded( _testRunStats ); - } - - private: - std::size_t counter = 0; - class AssertionPrinter { - public: - AssertionPrinter& operator= ( AssertionPrinter const& ) = delete; - AssertionPrinter( AssertionPrinter const& ) = delete; - AssertionPrinter( std::ostream& _stream, AssertionStats const& _stats, std::size_t _counter ) - : stream( _stream ) - , result( _stats.assertionResult ) - , messages( _stats.infoMessages ) - , itMessage( _stats.infoMessages.begin() ) - , printInfoMessages( true ) - , counter(_counter) - {} - - void print() { - itMessage = messages.begin(); - - switch( result.getResultType() ) { - case ResultWas::Ok: - printResultType( passedString() ); - printOriginalExpression(); - printReconstructedExpression(); - if ( ! result.hasExpression() ) - printRemainingMessages( Colour::None ); - else - printRemainingMessages(); - break; - case ResultWas::ExpressionFailed: - if (result.isOk()) { - printResultType(passedString()); - } else { - printResultType(failedString()); - } - printOriginalExpression(); - printReconstructedExpression(); - if (result.isOk()) { - printIssue(" # TODO"); - } - printRemainingMessages(); - break; - case ResultWas::ThrewException: - printResultType( failedString() ); - printIssue( "unexpected exception with message:" ); - printMessage(); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::FatalErrorCondition: - printResultType( failedString() ); - printIssue( "fatal error condition with message:" ); - printMessage(); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::DidntThrowException: - printResultType( failedString() ); - printIssue( "expected exception, got none" ); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::Info: - printResultType( "info" ); - printMessage(); - printRemainingMessages(); - break; - case ResultWas::Warning: - printResultType( "warning" ); - printMessage(); - printRemainingMessages(); - break; - case ResultWas::ExplicitFailure: - printResultType( failedString() ); - printIssue( "explicitly" ); - printRemainingMessages( Colour::None ); - break; - // These cases are here to prevent compiler warnings - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - printResultType( "** internal error **" ); - break; - } - } - - private: - static Colour::Code dimColour() { return Colour::FileName; } - - static const char* failedString() { return "not ok"; } - static const char* passedString() { return "ok"; } - - void printSourceInfo() const { - Colour colourGuard( dimColour() ); - stream << result.getSourceInfo() << ":"; - } - - void printResultType( std::string const& passOrFail ) const { - if( !passOrFail.empty() ) { - stream << passOrFail << ' ' << counter << " -"; - } - } - - void printIssue( std::string const& issue ) const { - stream << " " << issue; - } - - void printExpressionWas() { - if( result.hasExpression() ) { - stream << ";"; - { - Colour colour( dimColour() ); - stream << " expression was:"; - } - printOriginalExpression(); - } - } - - void printOriginalExpression() const { - if( result.hasExpression() ) { - stream << " " << result.getExpression(); - } - } - - void printReconstructedExpression() const { - if( result.hasExpandedExpression() ) { - { - Colour colour( dimColour() ); - stream << " for: "; - } - std::string expr = result.getExpandedExpression(); - std::replace( expr.begin(), expr.end(), '\n', ' '); - stream << expr; - } - } - - void printMessage() { - if ( itMessage != messages.end() ) { - stream << " '" << itMessage->message << "'"; - ++itMessage; - } - } - - void printRemainingMessages( Colour::Code colour = dimColour() ) { - if (itMessage == messages.end()) { - return; - } - - const auto itEnd = messages.cend(); - const auto N = static_cast( std::distance( itMessage, itEnd ) ); - - { - Colour colourGuard( colour ); - stream << " with " << pluralise( N, "message" ) << ":"; - } - - while( itMessage != itEnd ) { - // If this assertion is a warning ignore any INFO messages - if( printInfoMessages || itMessage->type != ResultWas::Info ) { - stream << " '" << itMessage->message << "'"; - if ( ++itMessage != itEnd ) { - Colour colourGuard( dimColour() ); - stream << " and"; - } - continue; - } - ++itMessage; - } - } - - private: - std::ostream& stream; - AssertionResult const& result; - std::vector messages; - std::vector::const_iterator itMessage; - bool printInfoMessages; - std::size_t counter; - }; - - void printTotals( const Totals& totals ) const { - stream << "1.." << totals.assertions.total(); - if( totals.testCases.total() == 0 ) { - stream << " # Skipped: No tests ran."; - } - } - }; - -#ifdef CATCH_IMPL - TAPReporter::~TAPReporter() {} -#endif - - CATCH_REGISTER_REPORTER( "tap", TAPReporter ) - -} // end namespace Catch - -#endif // TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED +/* + * Created by Colton Wolkins on 2015-08-15. + * Copyright 2015 Martin Moene. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED +#define TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED + + +// Don't #include any Catch headers here - we can assume they are already +// included before this header. +// This is not good practice in general but is necessary in this case so this +// file can be distributed as a single header that works with the main +// Catch single header. + +#include + +namespace Catch { + + struct TAPReporter : StreamingReporterBase { + + using StreamingReporterBase::StreamingReporterBase; + + TAPReporter( ReporterConfig const& config ): + StreamingReporterBase( config ) { + m_reporterPrefs.shouldReportAllAssertions = true; + } + + ~TAPReporter() override; + + static std::string getDescription() { + return "Reports test results in TAP format, suitable for test harnesses"; + } + + void noMatchingTestCases( std::string const& spec ) override { + stream << "# No test cases matched '" << spec << "'" << std::endl; + } + + void assertionStarting( AssertionInfo const& ) override {} + + bool assertionEnded( AssertionStats const& _assertionStats ) override { + ++counter; + + stream << "# " << currentTestCaseInfo->name << std::endl; + AssertionPrinter printer( stream, _assertionStats, counter ); + printer.print(); + + stream << std::endl; + return true; + } + + void testRunEnded( TestRunStats const& _testRunStats ) override { + printTotals( _testRunStats.totals ); + stream << "\n" << std::endl; + StreamingReporterBase::testRunEnded( _testRunStats ); + } + + private: + std::size_t counter = 0; + class AssertionPrinter { + public: + AssertionPrinter& operator= ( AssertionPrinter const& ) = delete; + AssertionPrinter( AssertionPrinter const& ) = delete; + AssertionPrinter( std::ostream& _stream, AssertionStats const& _stats, std::size_t _counter ) + : stream( _stream ) + , result( _stats.assertionResult ) + , messages( _stats.infoMessages ) + , itMessage( _stats.infoMessages.begin() ) + , printInfoMessages( true ) + , counter(_counter) + {} + + void print() { + itMessage = messages.begin(); + + switch( result.getResultType() ) { + case ResultWas::Ok: + printResultType( passedString() ); + printOriginalExpression(); + printReconstructedExpression(); + if ( ! result.hasExpression() ) + printRemainingMessages( Colour::None ); + else + printRemainingMessages(); + break; + case ResultWas::ExpressionFailed: + if (result.isOk()) { + printResultType(passedString()); + } else { + printResultType(failedString()); + } + printOriginalExpression(); + printReconstructedExpression(); + if (result.isOk()) { + printIssue(" # TODO"); + } + printRemainingMessages(); + break; + case ResultWas::ThrewException: + printResultType( failedString() ); + printIssue( "unexpected exception with message:" ); + printMessage(); + printExpressionWas(); + printRemainingMessages(); + break; + case ResultWas::FatalErrorCondition: + printResultType( failedString() ); + printIssue( "fatal error condition with message:" ); + printMessage(); + printExpressionWas(); + printRemainingMessages(); + break; + case ResultWas::DidntThrowException: + printResultType( failedString() ); + printIssue( "expected exception, got none" ); + printExpressionWas(); + printRemainingMessages(); + break; + case ResultWas::Info: + printResultType( "info" ); + printMessage(); + printRemainingMessages(); + break; + case ResultWas::Warning: + printResultType( "warning" ); + printMessage(); + printRemainingMessages(); + break; + case ResultWas::ExplicitFailure: + printResultType( failedString() ); + printIssue( "explicitly" ); + printRemainingMessages( Colour::None ); + break; + // These cases are here to prevent compiler warnings + case ResultWas::Unknown: + case ResultWas::FailureBit: + case ResultWas::Exception: + printResultType( "** internal error **" ); + break; + } + } + + private: + static Colour::Code dimColour() { return Colour::FileName; } + + static const char* failedString() { return "not ok"; } + static const char* passedString() { return "ok"; } + + void printSourceInfo() const { + Colour colourGuard( dimColour() ); + stream << result.getSourceInfo() << ":"; + } + + void printResultType( std::string const& passOrFail ) const { + if( !passOrFail.empty() ) { + stream << passOrFail << ' ' << counter << " -"; + } + } + + void printIssue( std::string const& issue ) const { + stream << " " << issue; + } + + void printExpressionWas() { + if( result.hasExpression() ) { + stream << ";"; + { + Colour colour( dimColour() ); + stream << " expression was:"; + } + printOriginalExpression(); + } + } + + void printOriginalExpression() const { + if( result.hasExpression() ) { + stream << " " << result.getExpression(); + } + } + + void printReconstructedExpression() const { + if( result.hasExpandedExpression() ) { + { + Colour colour( dimColour() ); + stream << " for: "; + } + std::string expr = result.getExpandedExpression(); + std::replace( expr.begin(), expr.end(), '\n', ' '); + stream << expr; + } + } + + void printMessage() { + if ( itMessage != messages.end() ) { + stream << " '" << itMessage->message << "'"; + ++itMessage; + } + } + + void printRemainingMessages( Colour::Code colour = dimColour() ) { + if (itMessage == messages.end()) { + return; + } + + const auto itEnd = messages.cend(); + const auto N = static_cast( std::distance( itMessage, itEnd ) ); + + { + Colour colourGuard( colour ); + stream << " with " << pluralise( N, "message" ) << ":"; + } + + while( itMessage != itEnd ) { + // If this assertion is a warning ignore any INFO messages + if( printInfoMessages || itMessage->type != ResultWas::Info ) { + stream << " '" << itMessage->message << "'"; + if ( ++itMessage != itEnd ) { + Colour colourGuard( dimColour() ); + stream << " and"; + } + continue; + } + ++itMessage; + } + } + + private: + std::ostream& stream; + AssertionResult const& result; + std::vector messages; + std::vector::const_iterator itMessage; + bool printInfoMessages; + std::size_t counter; + }; + + void printTotals( const Totals& totals ) const { + stream << "1.." << totals.assertions.total(); + if( totals.testCases.total() == 0 ) { + stream << " # Skipped: No tests ran."; + } + } + }; + +#ifdef CATCH_IMPL + TAPReporter::~TAPReporter() {} +#endif + + CATCH_REGISTER_REPORTER( "tap", TAPReporter ) + +} // end namespace Catch + +#endif // TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED diff --git a/third_party/catch2/include/catch2/catch_reporter_teamcity.hpp b/third_party/catch2/include/catch2/catch_reporter_teamcity.hpp index 5c985d2cc1..47b7e4aac3 100644 --- a/third_party/catch2/include/catch2/catch_reporter_teamcity.hpp +++ b/third_party/catch2/include/catch2/catch_reporter_teamcity.hpp @@ -1,219 +1,219 @@ -/* - * Created by Phil Nash on 19th December 2014 - * Copyright 2014 Two Blue Cubes Ltd. All rights reserved. - * - * Distributed under the Boost Software License, Version 1.0. (See accompanying - * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - */ -#ifndef TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED -#define TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED - -// Don't #include any Catch headers here - we can assume they are already -// included before this header. -// This is not good practice in general but is necessary in this case so this -// file can be distributed as a single header that works with the main -// Catch single header. - -#include - -#ifdef __clang__ -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wpadded" -#endif - -namespace Catch { - - struct TeamCityReporter : StreamingReporterBase { - TeamCityReporter( ReporterConfig const& _config ) - : StreamingReporterBase( _config ) - { - m_reporterPrefs.shouldRedirectStdOut = true; - } - - static std::string escape( std::string const& str ) { - std::string escaped = str; - replaceInPlace( escaped, "|", "||" ); - replaceInPlace( escaped, "'", "|'" ); - replaceInPlace( escaped, "\n", "|n" ); - replaceInPlace( escaped, "\r", "|r" ); - replaceInPlace( escaped, "[", "|[" ); - replaceInPlace( escaped, "]", "|]" ); - return escaped; - } - ~TeamCityReporter() override; - - static std::string getDescription() { - return "Reports test results as TeamCity service messages"; - } - - void skipTest( TestCaseInfo const& /* testInfo */ ) override { - } - - void noMatchingTestCases( std::string const& /* spec */ ) override {} - - void testGroupStarting( GroupInfo const& groupInfo ) override { - StreamingReporterBase::testGroupStarting( groupInfo ); - stream << "##teamcity[testSuiteStarted name='" - << escape( groupInfo.name ) << "']\n"; - } - void testGroupEnded( TestGroupStats const& testGroupStats ) override { - StreamingReporterBase::testGroupEnded( testGroupStats ); - stream << "##teamcity[testSuiteFinished name='" - << escape( testGroupStats.groupInfo.name ) << "']\n"; - } - - - void assertionStarting( AssertionInfo const& ) override {} - - bool assertionEnded( AssertionStats const& assertionStats ) override { - AssertionResult const& result = assertionStats.assertionResult; - if( !result.isOk() ) { - - ReusableStringStream msg; - if( !m_headerPrintedForThisSection ) - printSectionHeader( msg.get() ); - m_headerPrintedForThisSection = true; - - msg << result.getSourceInfo() << "\n"; - - switch( result.getResultType() ) { - case ResultWas::ExpressionFailed: - msg << "expression failed"; - break; - case ResultWas::ThrewException: - msg << "unexpected exception"; - break; - case ResultWas::FatalErrorCondition: - msg << "fatal error condition"; - break; - case ResultWas::DidntThrowException: - msg << "no exception was thrown where one was expected"; - break; - case ResultWas::ExplicitFailure: - msg << "explicit failure"; - break; - - // We shouldn't get here because of the isOk() test - case ResultWas::Ok: - case ResultWas::Info: - case ResultWas::Warning: - CATCH_ERROR( "Internal error in TeamCity reporter" ); - // These cases are here to prevent compiler warnings - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - CATCH_ERROR( "Not implemented" ); - } - if( assertionStats.infoMessages.size() == 1 ) - msg << " with message:"; - if( assertionStats.infoMessages.size() > 1 ) - msg << " with messages:"; - for( auto const& messageInfo : assertionStats.infoMessages ) - msg << "\n \"" << messageInfo.message << "\""; - - - if( result.hasExpression() ) { - msg << - "\n " << result.getExpressionInMacro() << "\n" - "with expansion:\n" << - " " << result.getExpandedExpression() << "\n"; - } - - if( currentTestCaseInfo->okToFail() ) { - msg << "- failure ignore as test marked as 'ok to fail'\n"; - stream << "##teamcity[testIgnored" - << " name='" << escape( currentTestCaseInfo->name )<< "'" - << " message='" << escape( msg.str() ) << "'" - << "]\n"; - } - else { - stream << "##teamcity[testFailed" - << " name='" << escape( currentTestCaseInfo->name )<< "'" - << " message='" << escape( msg.str() ) << "'" - << "]\n"; - } - } - stream.flush(); - return true; - } - - void sectionStarting( SectionInfo const& sectionInfo ) override { - m_headerPrintedForThisSection = false; - StreamingReporterBase::sectionStarting( sectionInfo ); - } - - void testCaseStarting( TestCaseInfo const& testInfo ) override { - m_testTimer.start(); - StreamingReporterBase::testCaseStarting( testInfo ); - stream << "##teamcity[testStarted name='" - << escape( testInfo.name ) << "']\n"; - stream.flush(); - } - - void testCaseEnded( TestCaseStats const& testCaseStats ) override { - StreamingReporterBase::testCaseEnded( testCaseStats ); - if( !testCaseStats.stdOut.empty() ) - stream << "##teamcity[testStdOut name='" - << escape( testCaseStats.testInfo.name ) - << "' out='" << escape( testCaseStats.stdOut ) << "']\n"; - if( !testCaseStats.stdErr.empty() ) - stream << "##teamcity[testStdErr name='" - << escape( testCaseStats.testInfo.name ) - << "' out='" << escape( testCaseStats.stdErr ) << "']\n"; - stream << "##teamcity[testFinished name='" - << escape( testCaseStats.testInfo.name ) << "' duration='" - << m_testTimer.getElapsedMilliseconds() << "']\n"; - stream.flush(); - } - - private: - void printSectionHeader( std::ostream& os ) { - assert( !m_sectionStack.empty() ); - - if( m_sectionStack.size() > 1 ) { - os << getLineOfChars<'-'>() << "\n"; - - std::vector::const_iterator - it = m_sectionStack.begin()+1, // Skip first section (test case) - itEnd = m_sectionStack.end(); - for( ; it != itEnd; ++it ) - printHeaderString( os, it->name ); - os << getLineOfChars<'-'>() << "\n"; - } - - SourceLineInfo lineInfo = m_sectionStack.front().lineInfo; - - os << lineInfo << "\n"; - os << getLineOfChars<'.'>() << "\n\n"; - } - - // if string has a : in first line will set indent to follow it on - // subsequent lines - static void printHeaderString( std::ostream& os, std::string const& _string, std::size_t indent = 0 ) { - std::size_t i = _string.find( ": " ); - if( i != std::string::npos ) - i+=2; - else - i = 0; - os << Column( _string ) - .indent( indent+i) - .initialIndent( indent ) << "\n"; - } - private: - bool m_headerPrintedForThisSection = false; - Timer m_testTimer; - }; - -#ifdef CATCH_IMPL - TeamCityReporter::~TeamCityReporter() {} -#endif - - CATCH_REGISTER_REPORTER( "teamcity", TeamCityReporter ) - -} // end namespace Catch - -#ifdef __clang__ -# pragma clang diagnostic pop -#endif - -#endif // TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED +/* + * Created by Phil Nash on 19th December 2014 + * Copyright 2014 Two Blue Cubes Ltd. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED +#define TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED + +// Don't #include any Catch headers here - we can assume they are already +// included before this header. +// This is not good practice in general but is necessary in this case so this +// file can be distributed as a single header that works with the main +// Catch single header. + +#include + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wpadded" +#endif + +namespace Catch { + + struct TeamCityReporter : StreamingReporterBase { + TeamCityReporter( ReporterConfig const& _config ) + : StreamingReporterBase( _config ) + { + m_reporterPrefs.shouldRedirectStdOut = true; + } + + static std::string escape( std::string const& str ) { + std::string escaped = str; + replaceInPlace( escaped, "|", "||" ); + replaceInPlace( escaped, "'", "|'" ); + replaceInPlace( escaped, "\n", "|n" ); + replaceInPlace( escaped, "\r", "|r" ); + replaceInPlace( escaped, "[", "|[" ); + replaceInPlace( escaped, "]", "|]" ); + return escaped; + } + ~TeamCityReporter() override; + + static std::string getDescription() { + return "Reports test results as TeamCity service messages"; + } + + void skipTest( TestCaseInfo const& /* testInfo */ ) override { + } + + void noMatchingTestCases( std::string const& /* spec */ ) override {} + + void testGroupStarting( GroupInfo const& groupInfo ) override { + StreamingReporterBase::testGroupStarting( groupInfo ); + stream << "##teamcity[testSuiteStarted name='" + << escape( groupInfo.name ) << "']\n"; + } + void testGroupEnded( TestGroupStats const& testGroupStats ) override { + StreamingReporterBase::testGroupEnded( testGroupStats ); + stream << "##teamcity[testSuiteFinished name='" + << escape( testGroupStats.groupInfo.name ) << "']\n"; + } + + + void assertionStarting( AssertionInfo const& ) override {} + + bool assertionEnded( AssertionStats const& assertionStats ) override { + AssertionResult const& result = assertionStats.assertionResult; + if( !result.isOk() ) { + + ReusableStringStream msg; + if( !m_headerPrintedForThisSection ) + printSectionHeader( msg.get() ); + m_headerPrintedForThisSection = true; + + msg << result.getSourceInfo() << "\n"; + + switch( result.getResultType() ) { + case ResultWas::ExpressionFailed: + msg << "expression failed"; + break; + case ResultWas::ThrewException: + msg << "unexpected exception"; + break; + case ResultWas::FatalErrorCondition: + msg << "fatal error condition"; + break; + case ResultWas::DidntThrowException: + msg << "no exception was thrown where one was expected"; + break; + case ResultWas::ExplicitFailure: + msg << "explicit failure"; + break; + + // We shouldn't get here because of the isOk() test + case ResultWas::Ok: + case ResultWas::Info: + case ResultWas::Warning: + CATCH_ERROR( "Internal error in TeamCity reporter" ); + // These cases are here to prevent compiler warnings + case ResultWas::Unknown: + case ResultWas::FailureBit: + case ResultWas::Exception: + CATCH_ERROR( "Not implemented" ); + } + if( assertionStats.infoMessages.size() == 1 ) + msg << " with message:"; + if( assertionStats.infoMessages.size() > 1 ) + msg << " with messages:"; + for( auto const& messageInfo : assertionStats.infoMessages ) + msg << "\n \"" << messageInfo.message << "\""; + + + if( result.hasExpression() ) { + msg << + "\n " << result.getExpressionInMacro() << "\n" + "with expansion:\n" << + " " << result.getExpandedExpression() << "\n"; + } + + if( currentTestCaseInfo->okToFail() ) { + msg << "- failure ignore as test marked as 'ok to fail'\n"; + stream << "##teamcity[testIgnored" + << " name='" << escape( currentTestCaseInfo->name )<< "'" + << " message='" << escape( msg.str() ) << "'" + << "]\n"; + } + else { + stream << "##teamcity[testFailed" + << " name='" << escape( currentTestCaseInfo->name )<< "'" + << " message='" << escape( msg.str() ) << "'" + << "]\n"; + } + } + stream.flush(); + return true; + } + + void sectionStarting( SectionInfo const& sectionInfo ) override { + m_headerPrintedForThisSection = false; + StreamingReporterBase::sectionStarting( sectionInfo ); + } + + void testCaseStarting( TestCaseInfo const& testInfo ) override { + m_testTimer.start(); + StreamingReporterBase::testCaseStarting( testInfo ); + stream << "##teamcity[testStarted name='" + << escape( testInfo.name ) << "']\n"; + stream.flush(); + } + + void testCaseEnded( TestCaseStats const& testCaseStats ) override { + StreamingReporterBase::testCaseEnded( testCaseStats ); + if( !testCaseStats.stdOut.empty() ) + stream << "##teamcity[testStdOut name='" + << escape( testCaseStats.testInfo.name ) + << "' out='" << escape( testCaseStats.stdOut ) << "']\n"; + if( !testCaseStats.stdErr.empty() ) + stream << "##teamcity[testStdErr name='" + << escape( testCaseStats.testInfo.name ) + << "' out='" << escape( testCaseStats.stdErr ) << "']\n"; + stream << "##teamcity[testFinished name='" + << escape( testCaseStats.testInfo.name ) << "' duration='" + << m_testTimer.getElapsedMilliseconds() << "']\n"; + stream.flush(); + } + + private: + void printSectionHeader( std::ostream& os ) { + assert( !m_sectionStack.empty() ); + + if( m_sectionStack.size() > 1 ) { + os << getLineOfChars<'-'>() << "\n"; + + std::vector::const_iterator + it = m_sectionStack.begin()+1, // Skip first section (test case) + itEnd = m_sectionStack.end(); + for( ; it != itEnd; ++it ) + printHeaderString( os, it->name ); + os << getLineOfChars<'-'>() << "\n"; + } + + SourceLineInfo lineInfo = m_sectionStack.front().lineInfo; + + os << lineInfo << "\n"; + os << getLineOfChars<'.'>() << "\n\n"; + } + + // if string has a : in first line will set indent to follow it on + // subsequent lines + static void printHeaderString( std::ostream& os, std::string const& _string, std::size_t indent = 0 ) { + std::size_t i = _string.find( ": " ); + if( i != std::string::npos ) + i+=2; + else + i = 0; + os << Column( _string ) + .indent( indent+i) + .initialIndent( indent ) << "\n"; + } + private: + bool m_headerPrintedForThisSection = false; + Timer m_testTimer; + }; + +#ifdef CATCH_IMPL + TeamCityReporter::~TeamCityReporter() {} +#endif + + CATCH_REGISTER_REPORTER( "teamcity", TeamCityReporter ) + +} // end namespace Catch + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + +#endif // TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED diff --git a/third_party/libyuv/Darwin/lib/libyuv.a b/third_party/libyuv/Darwin/lib/libyuv.a index 3265bd8da0..b72979ef19 100644 Binary files a/third_party/libyuv/Darwin/lib/libyuv.a and b/third_party/libyuv/Darwin/lib/libyuv.a differ diff --git a/third_party/libyuv/build.sh b/third_party/libyuv/build.sh old mode 100644 new mode 100755 diff --git a/third_party/libyuv/larch64/lib/libyuv.a b/third_party/libyuv/larch64/lib/libyuv.a index fdbcda2517..fd18814ef1 100644 Binary files a/third_party/libyuv/larch64/lib/libyuv.a and b/third_party/libyuv/larch64/lib/libyuv.a differ diff --git a/third_party/libyuv/x86_64/lib/libyuv.a b/third_party/libyuv/x86_64/lib/libyuv.a index ef786b1172..8915f167dc 100644 Binary files a/third_party/libyuv/x86_64/lib/libyuv.a and b/third_party/libyuv/x86_64/lib/libyuv.a differ diff --git a/third_party/mapbox-gl-native-qt/aarch64/libqmapboxgl.so b/third_party/mapbox-gl-native-qt/aarch64/libqmapboxgl.so index d417101ac0..508463c141 100755 Binary files a/third_party/mapbox-gl-native-qt/aarch64/libqmapboxgl.so and b/third_party/mapbox-gl-native-qt/aarch64/libqmapboxgl.so differ diff --git a/third_party/mapbox-gl-native-qt/x86_64/libqmapboxgl.so b/third_party/mapbox-gl-native-qt/x86_64/libqmapboxgl.so index 7637747a1a..a61c80c63d 100755 Binary files a/third_party/mapbox-gl-native-qt/x86_64/libqmapboxgl.so and b/third_party/mapbox-gl-native-qt/x86_64/libqmapboxgl.so differ diff --git a/third_party/qt5/larch64/bin/lrelease b/third_party/qt5/larch64/bin/lrelease index 5c0195d29a..5891f85851 100755 Binary files a/third_party/qt5/larch64/bin/lrelease and b/third_party/qt5/larch64/bin/lrelease differ diff --git a/third_party/qt5/larch64/bin/lupdate b/third_party/qt5/larch64/bin/lupdate index 67b7ea5cb5..0055aae229 100755 Binary files a/third_party/qt5/larch64/bin/lupdate and b/third_party/qt5/larch64/bin/lupdate differ diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so index d3872772bc..a1c6fed910 100644 Binary files a/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so and b/third_party/snpe/aarch64-ubuntu-gcc7.5/libPlatformValidatorShared.so differ diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so index be23b835ab..54c0c32e41 100644 Binary files a/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so and b/third_party/snpe/aarch64-ubuntu-gcc7.5/libSNPE.so differ diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so index 3b8c33f3f7..2154203026 100644 Binary files a/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so and b/third_party/snpe/aarch64-ubuntu-gcc7.5/libcalculator.so differ diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so index 8358969edc..1d81abd3a5 100644 Binary files a/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so and b/third_party/snpe/aarch64-ubuntu-gcc7.5/libhta.so differ diff --git a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so index c4f3fa67a2..40e3d5af74 100644 Binary files a/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so and b/third_party/snpe/aarch64-ubuntu-gcc7.5/libsnpe_dsp_domains_v2.so differ diff --git a/third_party/snpe/dsp/libcalculator_skel.so b/third_party/snpe/dsp/libcalculator_skel.so index 020dbfe869..9e9e9221be 100644 Binary files a/third_party/snpe/dsp/libcalculator_skel.so and b/third_party/snpe/dsp/libcalculator_skel.so differ diff --git a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so index c1eb88fc6a..3f1b7a8b8c 100644 Binary files a/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so and b/third_party/snpe/dsp/libsnpe_dsp_v65_domains_v2_skel.so differ diff --git a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so index eeeb447c60..aa42b5e830 100644 Binary files a/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so and b/third_party/snpe/dsp/libsnpe_dsp_v66_domains_v2_skel.so differ diff --git a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so index 028979f11f..9423da1b94 100644 Binary files a/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so and b/third_party/snpe/dsp/libsnpe_dsp_v68_domains_v3_skel.so differ diff --git a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp index e36b1fbd7e..ac33c84af8 100644 --- a/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp +++ b/third_party/snpe/include/DlSystem/IOBufferDataTypeMap.hpp @@ -1,127 +1,127 @@ -//============================================================================= -// -// Copyright (c) 2021-2022 Qualcomm Technologies, Inc. -// All Rights Reserved. -// Confidential and Proprietary - Qualcomm Technologies, Inc. -// -//============================================================================= - - -#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP -#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP - -#include -#include -#include "DlSystem/DlEnums.hpp" - -namespace DlSystem -{ - // Forward declaration of IOBufferDataTypeMapImpl implementation. - class IOBufferDataTypeMapImpl; -} - -namespace zdl -{ -namespace DlSystem -{ -/** @addtogroup c_plus_plus_apis C++ -@{ */ - -/** - * @brief . - * - * The IoBufferDataTypeMap class definition - */ -class ZDL_EXPORT IOBufferDataTypeMap final -{ -public: - - /** - * @brief . - * - * Creates a new Buffer Data type map - * - */ - IOBufferDataTypeMap(); - - /** - * @brief Adds a name and the corresponding buffer data type - * to the map - * - * @param[name] name The name of the buffer - * @param[bufferDataType] buffer Data Type of the buffer - * - * @note If a buffer with the same name already exists, no new - * buffer is added. - */ - void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType); - - /** - * @brief Removes a buffer name from the map - * - * @param[name] name The name of the buffer - * - */ - void remove(const char* name); - - /** - * @brief Returns the type of the named buffer - * - * @param[name] name The name of the buffer - * - * @return The type of the buffer, or UNSPECIFIED if the buffer does not exist - * - */ - zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name); - - /** - * @brief Returns the type of the first buffer - * - * @return The type of the first buffer, or UNSPECIFIED if the map is empty. - * - */ - zdl::DlSystem::IOBufferDataType_t getBufferDataType(); - - /** - * @brief Returns the size of the buffer type map. - * - * @return The size of the map - * - */ - size_t size(); - - /** - * @brief Checks the existence of the named buffer in the map - * - * @return True if the named buffer exists, false otherwise. - * - */ - bool find(const char* name); - - /** - * @brief Resets the map - * - */ - void clear(); - - /** - * @brief Checks whether the map is empty - * - * @return True if the map is empty, false otherwise. - * - */ - bool empty(); - - /** - * @brief Destroys the map - * - */ - ~IOBufferDataTypeMap(); - -private: - std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl; -}; -} - -} -#endif +//============================================================================= +// +// Copyright (c) 2021-2022 Qualcomm Technologies, Inc. +// All Rights Reserved. +// Confidential and Proprietary - Qualcomm Technologies, Inc. +// +//============================================================================= + + +#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP +#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP + +#include +#include +#include "DlSystem/DlEnums.hpp" + +namespace DlSystem +{ + // Forward declaration of IOBufferDataTypeMapImpl implementation. + class IOBufferDataTypeMapImpl; +} + +namespace zdl +{ +namespace DlSystem +{ +/** @addtogroup c_plus_plus_apis C++ +@{ */ + +/** + * @brief . + * + * The IoBufferDataTypeMap class definition + */ +class ZDL_EXPORT IOBufferDataTypeMap final +{ +public: + + /** + * @brief . + * + * Creates a new Buffer Data type map + * + */ + IOBufferDataTypeMap(); + + /** + * @brief Adds a name and the corresponding buffer data type + * to the map + * + * @param[name] name The name of the buffer + * @param[bufferDataType] buffer Data Type of the buffer + * + * @note If a buffer with the same name already exists, no new + * buffer is added. + */ + void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType); + + /** + * @brief Removes a buffer name from the map + * + * @param[name] name The name of the buffer + * + */ + void remove(const char* name); + + /** + * @brief Returns the type of the named buffer + * + * @param[name] name The name of the buffer + * + * @return The type of the buffer, or UNSPECIFIED if the buffer does not exist + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name); + + /** + * @brief Returns the type of the first buffer + * + * @return The type of the first buffer, or UNSPECIFIED if the map is empty. + * + */ + zdl::DlSystem::IOBufferDataType_t getBufferDataType(); + + /** + * @brief Returns the size of the buffer type map. + * + * @return The size of the map + * + */ + size_t size(); + + /** + * @brief Checks the existence of the named buffer in the map + * + * @return True if the named buffer exists, false otherwise. + * + */ + bool find(const char* name); + + /** + * @brief Resets the map + * + */ + void clear(); + + /** + * @brief Checks whether the map is empty + * + * @return True if the map is empty, false otherwise. + * + */ + bool empty(); + + /** + * @brief Destroys the map + * + */ + ~IOBufferDataTypeMap(); + +private: + std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl; +}; +} + +} +#endif diff --git a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so index 0b0657dd33..20d1377a01 100644 Binary files a/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so and b/third_party/snpe/x86_64-linux-clang/libHtpPrepare.so differ diff --git a/third_party/snpe/x86_64-linux-clang/libSNPE.so b/third_party/snpe/x86_64-linux-clang/libSNPE.so index 0cee9ac3af..0367b1106b 100644 Binary files a/third_party/snpe/x86_64-linux-clang/libSNPE.so and b/third_party/snpe/x86_64-linux-clang/libSNPE.so differ diff --git a/third_party/snpe/x86_64-linux-clang/libomp.so b/third_party/snpe/x86_64-linux-clang/libomp.so index 567e6ae594..db98cba162 100755 Binary files a/third_party/snpe/x86_64-linux-clang/libomp.so and b/third_party/snpe/x86_64-linux-clang/libomp.so differ diff --git a/tinygrad_repo b/tinygrad_repo index d8dda2af3a..ae5d1407ee 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit d8dda2af3afcef0bb772fff580cfa8b3eabf7f69 +Subproject commit ae5d1407ee844a97a52ad3756835d38e7e2b9e1b diff --git a/tools/README.md b/tools/README.md index 28f4f7ebd2..315db3a75f 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,55 +2,58 @@ ## System Requirements -openpilot is developed and tested on **Ubuntu 20.04**, which is the primary development target aside from the [supported embedded hardware](https://github.com/commaai/openpilot#running-on-a-dedicated-device-in-a-car). We also have a CI test to verify that openpilot builds on macOS, but the tools are untested. For the best experience, stick to Ubuntu 20.04, otherwise openpilot and the tools should work with minimal to no modifications on macOS and other Linux systems. +openpilot is developed and tested on **Ubuntu 20.04**, which is the primary development target aside from the [supported embedded hardware](https://github.com/commaai/openpilot#running-on-a-dedicated-device-in-a-car). -## Setup your PC +Running natively on any other system is not recommended and will require modifications. On Windows you can use WSL, and on macOS or incompatible Linux systems, it is recommended to use the dev containers. -First, clone openpilot: -``` bash -cd ~ -git clone --recurse-submodules https://github.com/commaai/openpilot.git +## Native setup on Ubuntu 20.04 + +**1. Clone openpilot** + +NOTE: This repository uses Git LFS for large files. Ensure you have [Git LFS](https://git-lfs.com/) installed and set up before cloning or working with it. -# or do a partial clone instead for a faster clone and smaller repo size +Either do a partial clone for faster download: +``` bash git clone --filter=blob:none --recurse-submodules --also-filter-submodules https://github.com/commaai/openpilot.git +``` -cd openpilot +or do a full clone: +``` bash +git clone --recurse-submodules https://github.com/commaai/openpilot.git ``` -Then, run the setup script: +**2. Run the setup script** ``` bash -# for Ubuntu 20.04 LTS +cd openpilot +git lfs pull tools/ubuntu_setup.sh - -# for macOS -tools/mac_setup.sh ``` Activate a shell with the Python dependencies installed: - ``` bash -cd openpilot && poetry shell +poetry shell ``` -Build openpilot with this command: +**3. Build openpilot** + ``` bash scons -u -j$(nproc) ``` -### Dev Container +## Dev Container on any Linux or macOS openpilot supports [Dev Containers](https://containers.dev/). Dev containers provide customizable and consistent development environment wrapped inside a container. This means you can develop in a designated environment matching our primary development target, regardless of your local setup. -Dev containers are supported in [multiple editors and IDEs](https://containers.dev/supporting), including [Visual Studio Code](https://code.visualstudio.com/docs/devcontainers/containers). +Dev containers are supported in [multiple editors and IDEs](https://containers.dev/supporting), including Visual Studio Code. Use the following [guide](https://code.visualstudio.com/docs/devcontainers/containers) to start using them with VSCode. #### X11 forwarding on macOS GUI apps like `ui` or `cabana` can also run inside the container by leveraging X11 forwarding. To make use of it on macOS, additional configuration steps must be taken. Follow [these](https://gist.github.com/sorny/969fe55d85c9b0035b0109a31cbcb088) steps to setup X11 forwarding on macOS. -### Windows +## WSL on Windows -Neither openpilot nor any of the tools are developed or tested on Windows, but the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/about) should provide a similar experience to native Ubuntu. [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/compare-versions) specifically has been reported by several users to be a seamless experience. +[Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/about) should provide a similar experience to native Ubuntu. [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/compare-versions) specifically has been reported by several users to be a seamless experience. Follow [these instructions](https://docs.microsoft.com/en-us/windows/wsl/install) to setup the WSL and install the `Ubuntu-20.04` distribution. Once your Ubuntu WSL environment is setup, follow the Linux setup instructions to finish setting up your environment. See [these instructions](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) for running GUI apps. diff --git a/tools/bodyteleop/static/js/webrtc.js b/tools/bodyteleop/static/js/webrtc.js index 8bc8e77317..e2f6583c17 100644 --- a/tools/bodyteleop/static/js/webrtc.js +++ b/tools/bodyteleop/static/js/webrtc.js @@ -89,18 +89,18 @@ export const constraints = { export function createDummyVideoTrack() { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); - + const frameWidth = 5; // Set the width of the frame const frameHeight = 5; // Set the height of the frame canvas.width = frameWidth; canvas.height = frameHeight; - + context.fillStyle = 'black'; context.fillRect(0, 0, frameWidth, frameHeight); - + const stream = canvas.captureStream(); const videoTrack = stream.getVideoTracks()[0]; - + return videoTrack; } @@ -108,37 +108,47 @@ export function createDummyVideoTrack() { export function start(pc, dc) { pc = createPeerConnection(pc); - if (constraints.audio || constraints.video) { - // add audio track - navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { - stream.getTracks().forEach(function(track) { - pc.addTrack(track, stream); - // only audio? - // if (track.kind === 'audio'){ - // pc.addTrack(track, stream); - // } - }); - return negotiate(pc); - }, function(err) { - alert('Could not acquire media: ' + err); - }); - - // add a fake video? - // const dummyVideoTrack = createDummyVideoTrack(); - // const dummyMediaStream = new MediaStream(); - // dummyMediaStream.addTrack(dummyVideoTrack); - // pc.addTrack(dummyVideoTrack, dummyMediaStream); - - } else { - negotiate(pc); - } - + // add audio track + navigator.mediaDevices.enumerateDevices() + .then(function(devices) { + const hasAudioInput = devices.find((device) => device.kind === "audioinput"); + var modifiedConstraints = {}; + modifiedConstraints.video = constraints.video; + modifiedConstraints.audio = hasAudioInput ? constraints.audio : false; + + return Promise.resolve(modifiedConstraints); + }) + .then(function(constraints) { + if (constraints.audio || constraints.video) { + return navigator.mediaDevices.getUserMedia(constraints); + } else{ + return Promise.resolve(null); + } + }) + .then(function(stream) { + if (stream) { + stream.getTracks().forEach(function(track) { + pc.addTrack(track, stream); + }); + } + + return negotiate(pc); + }) + .catch(function(err) { + alert('Could not acquire media: ' + err); + }); + + // add a fake video? + // const dummyVideoTrack = createDummyVideoTrack(); + // const dummyMediaStream = new MediaStream(); + // dummyMediaStream.addTrack(dummyVideoTrack); + // pc.addTrack(dummyVideoTrack, dummyMediaStream); + // setInterval(() => {pc.getStats(null).then((stats) => {stats.forEach((report) => console.log(report))})}, 10000) // var video = document.querySelector('video'); // var print = function (e, f){console.log(e, f); video.requestVideoFrameCallback(print);}; // video.requestVideoFrameCallback(print); - var parameters = {"ordered": true}; dc = pc.createDataChannel('data', parameters); dc.onclose = function() { @@ -157,7 +167,7 @@ export function start(pc, dc) { var message = JSON.stringify({type: 'battery_level'}); dc.send(message); } - + dc.onopen = function() { dcInterval = setInterval(controlCommand, 50); batteryInterval = setInterval(batteryLevel, 10000); diff --git a/tools/bodyteleop/web.py b/tools/bodyteleop/web.py index 717afdeaf8..929cfa26fe 100644 --- a/tools/bodyteleop/web.py +++ b/tools/bodyteleop/web.py @@ -18,11 +18,14 @@ import cereal.messaging as messaging from openpilot.common.basedir import BASEDIR from openpilot.tools.bodyteleop.bodyav import BodyMic, WebClientSpeaker, force_codec, play_sound, MediaBlackhole, EncodedBodyVideo +from typing import Optional + logger = logging.getLogger("pc") logging.basicConfig(level=logging.INFO) -pcs = set() -pm, sm = None, None +pcs: set[RTCPeerConnection] = set() +pm: Optional[messaging.PubMaster] = None +sm: Optional[messaging.SubMaster] = None TELEOPDIR = f"{BASEDIR}/tools/bodyteleop" diff --git a/tools/cabana/.gitignore b/tools/cabana/.gitignore index c3f5ef2b69..362a51f5c9 100644 --- a/tools/cabana/.gitignore +++ b/tools/cabana/.gitignore @@ -2,6 +2,5 @@ moc_* *.moc cabana -settings dbc/car_fingerprint_to_dbc.json tests/test_cabana diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index f1a7581870..d557b3af80 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -1,10 +1,7 @@ -import os -Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', - 'cereal', 'transformations', 'widgets') +Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal', 'widgets') base_frameworks = qt_env['FRAMEWORKS'] -base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', - 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] +base_libs = [common, messaging, cereal, visionipc, 'qt_util', 'zmq', 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] if arch == "Darwin": base_frameworks.append('OpenCL') @@ -18,8 +15,7 @@ else: qt_libs = ['qt_util'] + base_libs cabana_env = qt_env.Clone() -cabana_env["LIBPATH"] += ['../../opendbc/can'] -cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'panda', 'libdbc_static', 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'usb-1.0'] + qt_libs +cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'panda', 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] @@ -42,4 +38,4 @@ output_json_file = 'tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = cabana_env.Command('#' + output_json_file, ['dbc/generate_dbc_json.py'], "python3 tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file) -cabana_env.Depends(generate_dbc, ["#common", "#selfdrive/boardd", "#opendbc", "#cereal"]) +cabana_env.Depends(generate_dbc, ["#common", "#selfdrive/boardd", '#opendbc', "#cereal", Glob("#opendbc/*.dbc")]) diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index bd43c337bd..c72ebf9d35 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -24,6 +25,7 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { delegate = new BinaryItemDelegate(this); setItemDelegate(delegate); horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + horizontalHeader()->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); verticalHeader()->setSectionsClickable(false); verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); verticalHeader()->setDefaultSectionSize(CELL_HEIGHT); @@ -272,7 +274,7 @@ void BinaryViewModel::refresh() { row_count = can->lastMessage(msg_id).dat.size(); items.resize(row_count * column_count); } - int valid_rows = std::min(can->lastMessage(msg_id).dat.size(), row_count); + int valid_rows = std::min(can->lastMessage(msg_id).dat.size(), row_count); for (int i = 0; i < valid_rows * column_count; ++i) { items[i].valid = true; } @@ -280,7 +282,7 @@ void BinaryViewModel::refresh() { updateState(); } -void BinaryViewModel::updateItem(int row, int col, const QString &val, const QColor &color) { +void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &color) { auto &item = items[row * column_count + col]; if (item.val != val || item.bg_color != color) { item.val = val; @@ -307,17 +309,17 @@ void BinaryViewModel::updateState() { for (int i = 0; i < binary.size(); ++i) { for (int j = 0; j < 8; ++j) { auto &item = items[i * column_count + j]; - QString val = ((binary[i] >> (7 - j)) & 1) != 0 ? "1" : "0"; + int val = ((binary[i] >> (7 - j)) & 1) != 0 ? 1 : 0; // Bit update frequency based highlighting double offset = !item.sigs.empty() ? 50 : 0; - auto n = last_msg.bit_change_counts[i][7 - j]; + auto n = last_msg.last_changes[i].bit_change_counts[j]; double min_f = n == 0 ? offset : offset + 25; double alpha = std::clamp(offset + log2(1.0 + factor * (double)n / (double)last_msg.count) * scaler, min_f, max_f); auto color = item.bg_color; color.setAlpha(alpha); updateItem(i, j, val, color); } - updateItem(i, 8, toHex(binary[i]), last_msg.colors[i]); + updateItem(i, 8, binary[i], last_msg.colors[i]); } } @@ -333,13 +335,8 @@ QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, i } QVariant BinaryViewModel::data(const QModelIndex &index, int role) const { - if (role == Qt::ToolTipRole) { - auto item = (const BinaryViewModel::Item *)index.internalPointer(); - if (item && !item->sigs.empty()) { - return signalToolTip(item->sigs.back()); - } - } - return {}; + auto item = (const BinaryViewModel::Item *)index.internalPointer(); + return role == Qt::ToolTipRole && item && !item->sigs.empty() ? signalToolTip(item->sigs.back()) : QVariant(); } // BinaryItemDelegate @@ -348,6 +345,13 @@ BinaryItemDelegate::BinaryItemDelegate(QObject *parent) : QStyledItemDelegate(pa small_font.setPixelSize(8); hex_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); hex_font.setBold(true); + + bin_text_table[0].setText("0"); + bin_text_table[1].setText("1"); + for (int i = 0; i < 256; ++i) { + hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); + hex_text_table[i].prepare({}, hex_font); + } } bool BinaryItemDelegate::hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const { @@ -380,7 +384,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op drawSignalCell(painter, option, index, s); } } - } else if (item->valid) { + } else if (item->valid && item->bg_color.alpha() > 0) { painter->fillRect(option.rect, item->bg_color); } auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text; @@ -392,7 +396,9 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op } else if (!item->valid) { painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::BDiagPattern)); } - painter->drawText(option.rect, Qt::AlignCenter, item->val); + if (item->valid) { + utils::drawStaticText(painter, option.rect, index.column() == 8 ? hex_text_table[item->val] : bin_text_table[item->val]); + } if (item->is_msb || item->is_lsb) { painter->setFont(small_font); painter->drawText(option.rect.adjusted(8, 0, -8, -3), Qt::AlignRight | Qt::AlignBottom, item->is_msb ? "M" : "L"); diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index 04e1d5b2af..584910dc83 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -19,6 +19,8 @@ public: void drawSignalCell(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index, const cabana::Signal *sig) const; QFont small_font, hex_font; + std::array hex_text_table; + std::array bin_text_table; }; class BinaryViewModel : public QAbstractTableModel { @@ -26,7 +28,7 @@ public: BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {} void refresh(); void updateState(); - void updateItem(int row, int col, const QString &val, const QColor &color); + void updateItem(int row, int col, uint8_t val, const QColor &color); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } @@ -42,7 +44,7 @@ public: QColor bg_color = QColor(102, 86, 169, 255); bool is_msb = false; bool is_lsb = false; - QString val; + uint8_t val; QList sigs; bool valid = false; }; diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 7d3e6ab99f..205d795776 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -26,6 +26,7 @@ int main(int argc, char *argv[]) { cmd_parser.addOption({"demo", "use a demo route instead of providing your own"}); cmd_parser.addOption({"qcam", "load qcamera"}); cmd_parser.addOption({"ecam", "load wide road camera"}); + cmd_parser.addOption({"dcam", "load driver camera"}); cmd_parser.addOption({"stream", "read can messages from live streaming"}); cmd_parser.addOption({"panda", "read can messages from panda"}); cmd_parser.addOption({"panda-serial", "read can messages from panda with given serial", "panda-serial"}); @@ -60,13 +61,10 @@ int main(int argc, char *argv[]) { stream = new SocketCanStream(&app, config); } else { uint32_t replay_flags = REPLAY_FLAG_NONE; - if (cmd_parser.isSet("ecam")) { - replay_flags |= REPLAY_FLAG_ECAM; - } else if (cmd_parser.isSet("qcam")) { - replay_flags |= REPLAY_FLAG_QCAMERA; - } else if (cmd_parser.isSet("no-vipc")) { - replay_flags |= REPLAY_FLAG_NO_VIPC; - } + if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM; + if (cmd_parser.isSet("qcam")) replay_flags |= REPLAY_FLAG_QCAMERA; + if (cmd_parser.isSet("dcam")) replay_flags |= REPLAY_FLAG_DCAM; + if (cmd_parser.isSet("no-vipc")) replay_flags |= REPLAY_FLAG_NO_VIPC; const QStringList args = cmd_parser.positionalArguments(); QString route; @@ -75,12 +73,7 @@ int main(int argc, char *argv[]) { } else if (cmd_parser.isSet("demo")) { route = DEMO_ROUTE; } - - if (route.isEmpty()) { - StreamSelector dlg(&stream); - dlg.exec(); - dbc_file = dlg.dbcFile(); - } else { + if (!route.isEmpty()) { auto replay_stream = new ReplayStream(&app); if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { return 0; @@ -89,17 +82,28 @@ int main(int argc, char *argv[]) { } } - MainWindow w; - if (!stream) { - stream = new DummyStream(&app); - } - stream->start(); - if (!dbc_file.isEmpty()) { - w.loadFile(dbc_file); + int ret = 0; + { + MainWindow w; + QTimer::singleShot(0, [&]() { + if (!stream) { + StreamSelector dlg(&stream); + dlg.exec(); + dbc_file = dlg.dbcFile(); + } + if (!stream) { + stream = new DummyStream(&app); + } + stream->start(); + if (!dbc_file.isEmpty()) { + w.loadFile(dbc_file); + } + w.show(); + }); + + ret = app.exec(); } - w.show(); - int ret = app.exec(); delete can; return ret; } diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 18a9adbe07..6f08a9f20b 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -10,36 +10,37 @@ #include #include #include -#include #include #include #include +#include #include #include -#include #include #include "tools/cabana/chart/chartswidget.h" // ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html) const int AXIS_X_TOP_MARGIN = 4; -static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; } +// Define a small value of epsilon to compare double values +const float EPSILON = 0.000001; +static inline bool xLessThan(const QPointF &p, float x) { return p.x() < (x - EPSILON); } -ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) : charts_widget(parent), tip_label(this), QChartView(nullptr, parent) { +ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) + : charts_widget(parent), QChartView(parent) { series_type = (SeriesType)settings.chart_series_type; - QChart *chart = new QChart(); - chart->setBackgroundVisible(false); + chart()->setBackgroundVisible(false); axis_x = new QValueAxis(this); axis_y = new QValueAxis(this); - chart->addAxis(axis_x, Qt::AlignBottom); - chart->addAxis(axis_y, Qt::AlignLeft); - chart->legend()->layout()->setContentsMargins(0, 0, 0, 0); - chart->legend()->setShowToolTips(true); - chart->setMargins({0, 0, 0, 0}); + chart()->addAxis(axis_x, Qt::AlignBottom); + chart()->addAxis(axis_y, Qt::AlignLeft); + chart()->legend()->layout()->setContentsMargins(0, 0, 0, 0); + chart()->legend()->setShowToolTips(true); + chart()->setMargins({0, 0, 0, 0}); axis_x->setRange(x_range.first, x_range.second); - setChart(chart); + tip_label = new TipLabel(this); createToolButtons(); setRubberBand(QChartView::HorizontalRubberBand); setMouseTracking(true); @@ -65,8 +66,8 @@ void ChartView::createToolButtons() { close_btn_proxy->setWidget(remove_btn); close_btn_proxy->setZValue(chart()->zValue() + 11); + menu = new QMenu(this); // series types - QMenu *menu = new QMenu(this); auto change_series_group = new QActionGroup(menu); change_series_group->setExclusive(true); QStringList types{tr("Line"), tr("Step Line"), tr("Scatter")}; @@ -89,7 +90,9 @@ void ChartView::createToolButtons() { manage_btn_proxy->setWidget(manage_btn); manage_btn_proxy->setZValue(chart()->zValue() + 11); - QObject::connect(remove_btn, &QToolButton::clicked, [this]() { charts_widget->removeChart(this); }); + close_act = new QAction(tr("Close"), this); + QObject::connect(close_act, &QAction::triggered, [this] () { charts_widget->removeChart(this); }); + QObject::connect(remove_btn, &QToolButton::clicked, close_act, &QAction::triggered); QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { setSeriesType((SeriesType)action->data().toInt()); }); @@ -150,6 +153,11 @@ void ChartView::removeIf(std::function predicate) { void ChartView::signalUpdated(const cabana::Signal *sig) { if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.sig == sig; })) { + for (const auto &s : sigs) { + if (s.sig == sig && s.series->color() != sig->color) { + setSeriesColor(s.series, sig->color); + } + } updateTitle(); updateSeries(sig); } @@ -162,7 +170,7 @@ void ChartView::msgUpdated(MessageId id) { } void ChartView::manageSignals() { - SignalSelector dlg(tr("Mange Chart"), this); + SignalSelector dlg(tr("Manage Chart"), this); for (auto &s : sigs) { dlg.addSelected(s.msg_id, s.sig); } @@ -272,41 +280,50 @@ void ChartView::updateSeriesPoints() { } } -void ChartView::updateSeries(const cabana::Signal *sig, bool clear) { +void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector &events, + std::vector &vals, std::vector &step_vals) { + vals.reserve(vals.size() + events.capacity()); + step_vals.reserve(step_vals.size() + events.capacity() * 2); + + double value = 0; + const uint64_t begin_mono_time = can->routeStartTime() * 1e9; + for (const CanEvent *e : events) { + if (sig->getValue(e->dat, e->size, &value)) { + const double ts = (e->mono_time - std::min(e->mono_time, begin_mono_time)) / 1e9; + vals.emplace_back(ts, value); + if (!step_vals.empty()) + step_vals.emplace_back(ts, step_vals.back().y()); + step_vals.emplace_back(ts, value); + } + } +} + +void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) { for (auto &s : sigs) { if (!sig || s.sig == sig) { - if (clear) { + if (!msg_new_events) { s.vals.clear(); s.step_vals.clear(); - s.last_value_mono_time = 0; } - s.series->setColor(s.sig->color); - - const auto &msgs = can->events(s.msg_id); - s.vals.reserve(msgs.capacity()); - s.step_vals.reserve(msgs.capacity() * 2); - - auto first = std::upper_bound(msgs.cbegin(), msgs.cend(), s.last_value_mono_time, [](uint64_t ts, auto e) { - return ts < e->mono_time; - }); - const double route_start_time = can->routeStartTime(); - for (auto end = msgs.cend(); first != end; ++first) { - const CanEvent *e = *first; - double value = 0; - if (s.sig->getValue(e->dat, e->size, &value)) { - double ts = e->mono_time / 1e9 - route_start_time; // seconds - s.vals.append({ts, value}); - if (!s.step_vals.empty()) { - s.step_vals.append({ts, s.step_vals.back().y()}); - } - s.step_vals.append({ts, value}); - s.last_value_mono_time = e->mono_time; - } + auto events = msg_new_events ? msg_new_events : &can->eventsMap(); + auto it = events->find(s.msg_id); + if (it == events->end() || it->second.empty()) continue; + + if (s.vals.empty() || (it->second.back()->mono_time / 1e9 - can->routeStartTime()) > s.vals.back().x()) { + appendCanEvents(s.sig, it->second, s.vals, s.step_vals); + } else { + std::vector vals, step_vals; + appendCanEvents(s.sig, it->second, vals, step_vals); + s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x(), xLessThan), + vals.begin(), vals.end()); + s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x(), xLessThan), + step_vals.begin(), step_vals.end()); } + if (!can->liveStreaming()) { s.segment_tree.build(s.vals); } - s.series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); + s.series->replace(QVector::fromStdVector(series_type == SeriesType::StepLine ? s.step_vals : s.vals)); } } updateAxisY(); @@ -316,7 +333,7 @@ void ChartView::updateSeries(const cabana::Signal *sig, bool clear) { // auto zoom on yaxis void ChartView::updateAxisY() { - if (sigs.isEmpty()) return; + if (sigs.empty()) return; double min = std::numeric_limits::max(); double max = std::numeric_limits::lowest(); @@ -340,9 +357,7 @@ void ChartView::updateAxisY() { if (it->y() > s.max) s.max = it->y(); } } else { - auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last)); - s.min = min_y; - s.max = max_y; + std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last)); } min = std::min(min, s.min); max = std::max(max, s.max); @@ -361,7 +376,7 @@ void ChartView::updateAxisY() { axis_y->setRange(min_y, max_y); axis_y->setTickCount(tick_count); - int n = qMax(int(-qFloor(std::log10((max_y - min_y) / (tick_count - 1)))), 0) + 1; + int n = std::max(int(-std::floor(std::log10((max_y - min_y) / (tick_count - 1)))), 0); int max_label_width = 0; QFontMetrics fm(axis_y->labelsFont()); for (int i = 0; i < tick_count; i++) { @@ -379,15 +394,15 @@ void ChartView::updateAxisY() { std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) { qreal range = niceNumber((max - min), true); // range with ceiling qreal step = niceNumber(range / (tick_count - 1), false); - min = qFloor(min / step); - max = qCeil(max / step); + min = std::floor(min / step); + max = std::ceil(max / step); tick_count = int(max - min) + 1; return {min * step, max * step, tick_count}; } // nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n qreal ChartView::niceNumber(qreal x, bool ceiling) { - qreal z = qPow(10, qFloor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x + qreal z = std::pow(10, std::floor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x qreal q = x / z; //q<10 && q>=1; if (ceiling) { if (q <= 1.0) q = 1; @@ -404,7 +419,7 @@ qreal ChartView::niceNumber(qreal x, bool ceiling) { } void ChartView::leaveEvent(QEvent *event) { - if (tip_label.isVisible()) { + if (tip_label->isVisible()) { charts_widget->showValueTip(-1); } QChartView::leaveEvent(event); @@ -449,6 +464,17 @@ static QPixmap getDropPixmap(const QPixmap &src) { return px; } +void ChartView::contextMenuEvent(QContextMenuEvent *event) { + QMenu context_menu(this); + context_menu.addActions(menu->actions()); + context_menu.addSeparator(); + context_menu.addAction(charts_widget->undo_zoom_action); + context_menu.addAction(charts_widget->redo_zoom_action); + context_menu.addSeparator(); + context_menu.addAction(close_act); + context_menu.exec(event->globalPos()); +} + void ChartView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && move_icon->sceneBoundingRect().contains(event->pos())) { QMimeData *mimeData = new QMimeData; @@ -476,14 +502,10 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) { auto rubber = findChild(); if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { rubber->hide(); - QRectF rect = rubber->geometry().normalized(); - double min = chart()->mapToValue(rect.topLeft()).x(); - double max = chart()->mapToValue(rect.bottomRight()).x(); - + auto rect = rubber->geometry().normalized(); // Prevent zooming/seeking past the end of the route - min = std::clamp(min, 0., can->totalSeconds()); - max = std::clamp(max, 0., can->totalSeconds()); - + double min = std::clamp(chart()->mapToValue(rect.topLeft()).x(), 0., can->totalSeconds()); + double max = std::clamp(chart()->mapToValue(rect.bottomRight()).x(), 0., can->totalSeconds()); if (rubber->width() <= 0) { // no rubber dragged, seek to mouse position can->seekTo(min); @@ -524,7 +546,7 @@ void ChartView::mouseMoveEvent(QMouseEvent *ev) { if (!is_zooming && plot_area.contains(ev->pos())) { const double sec = chart()->mapToValue(ev->pos()).x(); charts_widget->showValueTip(sec); - } else if (tip_label.isVisible()) { + } else if (tip_label->isVisible()) { charts_widget->showValueTip(-1); } @@ -544,7 +566,7 @@ void ChartView::showTip(double sec) { QRect tip_area(0, chart()->plotArea().top(), rect().width(), chart()->plotArea().height()); QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area); if (visible_rect.isEmpty()) { - tip_label.hide(); + tip_label->hide(); return; } @@ -574,14 +596,14 @@ void ChartView::showTip(double sec) { QPoint pt(x, chart()->plotArea().top()); text_list.push_front(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3)); QString text = "

" % text_list.join("
") % "

"; - tip_label.showText(pt, text, this, visible_rect); + tip_label->showText(pt, text, this, visible_rect); viewport()->update(); } void ChartView::hideTip() { clearTrackPoints(); tooltip_x = -1; - tip_label.hide(); + tip_label->hide(); viewport()->update(); } @@ -608,7 +630,7 @@ void ChartView::dropEvent(QDropEvent *event) { source_chart->chart()->removeSeries(s.series); addSeries(s.series); } - sigs.append(source_chart->sigs); + sigs.insert(sigs.end(), std::move_iterator(source_chart->sigs.begin()), std::move_iterator(source_chart->sigs.end())); updateAxisY(); updateTitle(); startAnimation(); @@ -670,6 +692,7 @@ void ChartView::drawBackground(QPainter *painter, const QRectF &rect) { void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { drawTimeline(painter); + drawSignalValue(painter); // draw track points painter->setPen(Qt::NoPen); qreal track_line_x = -1; @@ -700,7 +723,10 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { } } - // paint zoom range + drawRubberBandTimeRange(painter); +} + +void ChartView::drawRubberBandTimeRange(QPainter *painter) { auto rubber = findChild(); if (rubber && rubber->isVisible() && rubber->width() > 1) { painter->setPen(Qt::white); @@ -717,23 +743,24 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { void ChartView::drawTimeline(QPainter *painter) { const auto plot_area = chart()->plotArea(); - // draw line + // draw vertical time line qreal x = std::clamp(chart()->mapToPosition(QPointF{cur_sec, 0}).x(), plot_area.left(), plot_area.right()); painter->setPen(QPen(chart()->titleBrush().color(), 2)); painter->drawLine(QPointF{x, plot_area.top()}, QPointF{x, plot_area.bottom() + 1}); - // draw current time + // draw current time under the axis-x QString time_str = QString::number(cur_sec, 'f', 2); QSize time_str_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, time_str) + QSize(8, 2); - QRect time_str_rect(QPoint(x - time_str_size.width() / 2, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); + QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); QPainterPath path; path.addRoundedRect(time_str_rect, 3, 3); painter->fillPath(path, settings.theme == DARK_THEME ? Qt::darkGray : Qt::gray); painter->setPen(palette().color(QPalette::BrightText)); painter->setFont(axis_x->labelsFont()); painter->drawText(time_str_rect, Qt::AlignCenter, time_str); +} - // draw signal value +void ChartView::drawSignalValue(QPainter *painter) { auto item_group = qgraphicsitem_cast(chart()->legend()->childItems()[0]); assert(item_group != nullptr); auto legend_markers = item_group->childItems(); @@ -743,13 +770,9 @@ void ChartView::drawTimeline(QPainter *painter) { painter->setPen(chart()->legend()->labelColor()); int i = 0; for (auto &s : sigs) { - QString value = "--"; - if (s.series->isVisible()) { - auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, [](auto &p, double x) { return p.x() > x; }); - if (it != s.vals.crend() && it->x() >= axis_x->min()) { - value = s.sig->formatValue(it->y()); - } - } + auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, + [](auto &p, double x) { return p.x() > x + EPSILON; }); + QString value = (it != s.vals.crend() && it->x() >= axis_x->min()) ? s.sig->formatValue(it->y()) : "--"; QRectF marker_rect = legend_markers[i++]->sceneBoundingRect(); QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size()); QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width()); @@ -785,6 +808,7 @@ QXYSeries *ChartView::createSeries(SeriesType type, QColor color) { } void ChartView::addSeries(QXYSeries *series) { + setSeriesColor(series, series->color()); chart()->addSeries(series); series->attachAxis(axis_x); series->attachAxis(axis_y); @@ -797,6 +821,21 @@ void ChartView::addSeries(QXYSeries *series) { } } +void ChartView::setSeriesColor(QXYSeries *series, QColor color) { + auto existing_series = chart()->series(); + for (auto s : existing_series) { + if (s != series && std::abs(color.hueF() - qobject_cast(s)->color().hueF()) < 0.1) { + // use different color to distinguish it from others. + auto last_color = qobject_cast(existing_series.back())->color(); + color.setHsvF(std::fmod(last_color.hueF() + 60 / 360.0, 1.0), + QRandomGenerator::global()->bounded(35, 100) / 100.0, + QRandomGenerator::global()->bounded(85, 100) / 100.0); + break; + } + } + series->setColor(color); +} + void ChartView::setSeriesType(SeriesType type) { if (type != series_type) { series_type = type; @@ -805,9 +844,8 @@ void ChartView::setSeriesType(SeriesType type) { s.series->deleteLater(); } for (auto &s : sigs) { - auto series = createSeries(series_type, s.sig->color); - series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); - s.series = series; + s.series = createSeries(series_type, s.sig->color); + s.series->replace(QVector::fromStdVector(series_type == SeriesType::StepLine ? s.step_vals : s.vals)); } updateSeriesPoints(); updateTitle(); diff --git a/tools/cabana/chart/chart.h b/tools/cabana/chart/chart.h index 2740d81b66..d690a14d1c 100644 --- a/tools/cabana/chart/chart.h +++ b/tools/cabana/chart/chart.h @@ -2,7 +2,9 @@ #include #include +#include +#include #include #include #include @@ -30,7 +32,7 @@ public: ChartView(const std::pair &x_range, ChartsWidget *parent = nullptr); void addSignal(const MessageId &msg_id, const cabana::Signal *sig); bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const; - void updateSeries(const cabana::Signal *sig = nullptr, bool clear = true); + void updateSeries(const cabana::Signal *sig = nullptr, const MessageEventsMap *msg_new_events = nullptr); void updatePlot(double cur, double min, double max); void setSeriesType(SeriesType type); void updatePlotArea(int left, bool force = false); @@ -42,9 +44,8 @@ public: MessageId msg_id; const cabana::Signal *sig = nullptr; QXYSeries *series = nullptr; - QVector vals; - QVector step_vals; - uint64_t last_value_mono_time = 0; + std::vector vals; + std::vector step_vals; QPointF track_pt{}; SegmentTree segment_tree; double min = 0; @@ -63,8 +64,11 @@ private slots: void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } private: + void appendCanEvents(const cabana::Signal *sig, const std::vector &events, + std::vector &vals, std::vector &step_vals); void createToolButtons(); void addSeries(QXYSeries *series); + void contextMenuEvent(QContextMenuEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *ev) override; @@ -83,10 +87,13 @@ private: void drawForeground(QPainter *painter, const QRectF &rect) override; void drawBackground(QPainter *painter, const QRectF &rect) override; void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); } + void drawSignalValue(QPainter *painter); void drawTimeline(QPainter *painter); + void drawRubberBandTimeRange(QPainter *painter); std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); qreal niceNumber(qreal x, bool ceiling); QXYSeries *createSeries(SeriesType type, QColor color); + void setSeriesColor(QXYSeries *, QColor color); void updateSeriesPoints(); void removeIf(std::function predicate); inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } @@ -95,12 +102,14 @@ private: int align_to = 0; QValueAxis *axis_x; QValueAxis *axis_y; + QMenu *menu; QAction *split_chart_act; + QAction *close_act; QGraphicsPixmapItem *move_icon; QGraphicsProxyWidget *close_btn_proxy; QGraphicsProxyWidget *manage_btn_proxy; - TipLabel tip_label; - QList sigs; + TipLabel *tip_label; + std::vector sigs; double cur_sec = 0; SeriesType series_type = SeriesType::Line; bool is_scrubbing = false; diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index e852e6d206..63d6091cac 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -12,9 +12,11 @@ #include "tools/cabana/chart/chart.h" const int MAX_COLUMN_COUNT = 4; -const int CHART_SPACING = 10; +const int CHART_SPACING = 4; -ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_timer(this), QFrame(parent) { +ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { + align_timer = new QTimer(this); + auto_scroll_timer = new QTimer(this); setFrameStyle(QFrame::StyledPanel | QFrame::Plain); QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); @@ -78,8 +80,9 @@ ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_tim // charts charts_container = new ChartsContainer(this); - + charts_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); charts_scroll = new QScrollArea(this); + charts_scroll->viewport()->setBackgroundRole(QPalette::Base); charts_scroll->setFrameStyle(QFrame::NoFrame); charts_scroll->setWidgetResizable(true); charts_scroll->setWidget(charts_container); @@ -94,12 +97,12 @@ ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_tim range_slider->setValue(max_chart_range); updateToolBar(); - align_timer.setSingleShot(true); - QObject::connect(&align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); - QObject::connect(&auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); + align_timer->setSingleShot(true); + QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); + QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged); - QObject::connect(can, &AbstractStream::updated, this, &ChartsWidget::updateState); + QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState); QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange); QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart); QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll); @@ -149,11 +152,10 @@ void ChartsWidget::updateTabBar() { } } -void ChartsWidget::eventsMerged() { +void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) { QFutureSynchronizer future_synchronizer; - bool clear = !can->liveStreaming(); for (auto c : charts) { - future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr, clear)); + future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr, &new_events)); } } @@ -190,7 +192,7 @@ void ChartsWidget::updateState() { if (pos < 0 || pos > 0.8) { display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); } - double max_sec = std::min(std::floor(display_range.first + max_chart_range), can->totalSeconds()); + double max_sec = std::min(display_range.first + max_chart_range, can->totalSeconds()); display_range.first = std::max(0.0, max_sec - max_chart_range); display_range.second = display_range.first + max_chart_range; } else if (cur_sec < (zoomed_range.first - 0.1) || cur_sec >= zoomed_range.second) { @@ -219,7 +221,7 @@ void ChartsWidget::updateToolBar() { undo_zoom_action->setVisible(is_zoomed); redo_zoom_action->setVisible(is_zoomed); reset_zoom_action->setVisible(is_zoomed); - reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(zoomed_range.first, 0, 'f', 1).arg(zoomed_range.second, 0, 'f', 1) : ""); + reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(zoomed_range.first, 0, 'f', 2).arg(zoomed_range.second, 0, 'f', 2) : ""); remove_all_btn->setEnabled(!charts.isEmpty()); dock_btn->setIcon(docking ? "arrow-up-right-square" : "arrow-down-left-square"); dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); @@ -253,7 +255,7 @@ ChartView *ChartsWidget::createChart() { chart->setFixedHeight(settings.chart_height); chart->setMinimumWidth(CHART_MIN_WIDTH); chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - QObject::connect(chart, &ChartView::axisYLabelWidthChanged, &align_timer, qOverload<>(&QTimer::start)); + QObject::connect(chart, &ChartView::axisYLabelWidthChanged, align_timer, qOverload<>(&QTimer::start)); charts.push_front(chart); currentCharts().push_front(chart); updateLayout(true); @@ -277,14 +279,19 @@ void ChartsWidget::splitChart(ChartView *src_chart) { for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) { auto c = createChart(); src_chart->chart()->removeSeries(it->series); + + // Restore to the original color + it->series->setColor(it->sig->color); + c->addSeries(it->series); - c->sigs.push_back(*it); + c->sigs.emplace_back(std::move(*it)); c->updateAxisY(); c->updateTitle(); it = src_chart->sigs.erase(it); } src_chart->updateAxisY(); src_chart->updateTitle(); + QTimer::singleShot(0, src_chart, &ChartView::resetChartCache); } } @@ -317,9 +324,9 @@ void ChartsWidget::updateLayout(bool force) { } for (int i = 0; i < current_charts.size(); ++i) { charts_layout->addWidget(current_charts[i], i / n, i % n); - if (current_charts[i]->sigs.isEmpty()) { + if (current_charts[i]->sigs.empty()) { // the chart will be resized after add signal. delay setVisible to reduce flicker. - QTimer::singleShot(0, [c = current_charts[i]]() { c->setVisible(true); }); + QTimer::singleShot(0, current_charts[i], [c = current_charts[i]]() { c->setVisible(true); }); } else { current_charts[i]->setVisible(true); } @@ -329,11 +336,11 @@ void ChartsWidget::updateLayout(bool force) { } void ChartsWidget::startAutoScroll() { - auto_scroll_timer.start(50); + auto_scroll_timer->start(50); } void ChartsWidget::stopAutoScroll() { - auto_scroll_timer.stop(); + auto_scroll_timer->stop(); auto_scroll_count = 0; } @@ -365,6 +372,10 @@ void ChartsWidget::doAutoScroll() { } } +QSize ChartsWidget::minimumSizeHint() const { + return QSize(CHART_MIN_WIDTH, QWidget::minimumSizeHint().height()); +} + void ChartsWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); updateLayout(); @@ -400,16 +411,15 @@ void ChartsWidget::removeAll() { tabbar->removeTab(1); } tab_charts.clear(); - zoomReset(); if (!charts.isEmpty()) { for (auto c : charts) { delete c; } charts.clear(); - updateToolBar(); emit seriesChanged(); } + zoomReset(); } void ChartsWidget::alignCharts() { @@ -466,8 +476,9 @@ bool ChartsWidget::event(QEvent *event) { ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) { setAcceptDrops(true); + setBackgroundRole(QPalette::Window); QVBoxLayout *charts_main_layout = new QVBoxLayout(this); - charts_main_layout->setContentsMargins(0, 10, 0, 0); + charts_main_layout->setContentsMargins(0, CHART_SPACING, 0, CHART_SPACING); charts_layout = new QGridLayout(); charts_layout->setSpacing(CHART_SPACING); charts_main_layout->addLayout(charts_layout); @@ -511,15 +522,11 @@ void ChartsContainer::paintEvent(QPaintEvent *ev) { r.setHeight(CHART_SPACING); } - const int margin = (CHART_SPACING - 2) / 2; - QPainterPath path; - path.addPolygon(QPolygonF({r.topLeft(), QPointF(r.left() + CHART_SPACING, r.top() + r.height() / 2), r.bottomLeft()})); - path.addPolygon(QPolygonF({r.topRight(), QPointF(r.right() - CHART_SPACING, r.top() + r.height() / 2), r.bottomRight()})); - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - p.fillPath(path, palette().highlight()); - p.fillRect(r.adjusted(2, margin, -2, -margin), palette().highlight()); + p.setPen(QPen(palette().highlight(), 2)); + p.drawLine(r.topLeft() + QPoint(1, 0), r.bottomLeft() + QPoint(1, 0)); + p.drawLine(r.topLeft() + QPoint(0, r.height() / 2), r.topRight() + QPoint(0, r.height() / 2)); + p.drawLine(r.topRight(), r.bottomRight()); } } diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h index 88ba4226c0..a39b4d73a5 100644 --- a/tools/cabana/chart/chartswidget.h +++ b/tools/cabana/chart/chartswidget.h @@ -54,6 +54,7 @@ signals: void seriesChanged(); private: + QSize minimumSizeHint() const override; void resizeEvent(QResizeEvent *event) override; bool event(QEvent *event) override; void alignCharts(); @@ -62,7 +63,7 @@ private: void removeChart(ChartView *chart); void splitChart(ChartView *chart); QRect chartVisibleRect(ChartView *chart); - void eventsMerged(); + void eventsMerged(const MessageEventsMap &new_events); void updateState(); void zoomReset(); void startAutoScroll(); @@ -108,8 +109,8 @@ private: int column_count = 1; int current_column_count = 0; int auto_scroll_count = 0; - QTimer auto_scroll_timer; - QTimer align_timer; + QTimer *auto_scroll_timer; + QTimer *align_timer; int current_theme = 0; friend class ZoomCommand; friend class ChartView; @@ -120,7 +121,7 @@ class ZoomCommand : public QUndoCommand { public: ZoomCommand(ChartsWidget *charts, std::pair range) : charts(charts), range(range), QUndoCommand() { prev_range = charts->is_zoomed ? charts->zoomed_range : charts->display_range; - setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 1).arg(range.second, 0, 'f', 1)); + setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 2).arg(range.second, 0, 'f', 2)); } void undo() override { charts->setZoom(prev_range.first, prev_range.second); } void redo() override { charts->setZoom(range.first, range.second); } diff --git a/tools/cabana/chart/signalselector.cc b/tools/cabana/chart/signalselector.cc index 50fe861a03..0ddb212a8a 100644 --- a/tools/cabana/chart/signalselector.cc +++ b/tools/cabana/chart/signalselector.cc @@ -44,9 +44,9 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); main_layout->addWidget(buttonBox, 3, 2); - for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) { - if (auto m = dbc()->msg(it.key())) { - msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(it.key().toString()), QVariant::fromValue(it.key())); + for (const auto &[id, _] : can->lastMessages()) { + if (auto m = dbc()->msg(id)) { + msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(id.toString()), QVariant::fromValue(id)); } } msgs_combo->model()->sort(0); diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 4692b41e4b..42608c030f 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -1,5 +1,6 @@ #include "tools/cabana/chart/sparkline.h" +#include #include #include @@ -9,59 +10,33 @@ void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, doubl const auto &msgs = can->events(msg_id); uint64_t ts = (last_msg_ts + can->routeStartTime()) * 1e9; uint64_t first_ts = (ts > range * 1e9) ? ts - range * 1e9 : 0; - auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), first_ts, [](auto e, uint64_t ts) { - return e->mono_time < ts; - }); - auto last = std::upper_bound(first, msgs.cend(), ts, [](uint64_t ts, auto e) { - return ts < e->mono_time; - }); - - bool update_values = last_ts != last_msg_ts || time_range != range; - last_ts = last_msg_ts; - time_range = range; - - if (first != last) { - if (update_values) { - values.clear(); - if (values.capacity() < std::distance(first, last)) { - values.reserve(std::distance(first, last) * 2); - } - min_val = std::numeric_limits::max(); - max_val = std::numeric_limits::lowest(); - for (auto it = first; it != last; ++it) { - const CanEvent *e = *it; - double value = 0; - if (sig->getValue(e->dat, e->size, &value)) { - values.emplace_back((e->mono_time - (*first)->mono_time) / 1e9, value); - if (min_val > value) min_val = value; - if (max_val < value) max_val = value; - } - } - if (min_val == max_val) { - min_val -= 1; - max_val += 1; + auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), first_ts, CompareCanEvent()); + auto last = std::upper_bound(first, msgs.cend(), ts, CompareCanEvent()); + + if (first != last && !size.isEmpty()) { + points.clear(); + double value = 0; + for (auto it = first; it != last; ++it) { + if (sig->getValue((*it)->dat, (*it)->size, &value)) { + points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); } } - } else { - values.clear(); - } - - if (!values.empty()) { - render(sig->color, size); + const auto [min, max] = std::minmax_element(points.begin(), points.end(), + [](auto &l, auto &r) { return l.y() < r.y(); }); + min_val = min->y() == max->y() ? min->y() - 1 : min->y(); + max_val = min->y() == max->y() ? max->y() + 1 : max->y(); + freq_ = points.size() / std::max(points.back().x() - points.front().x(), 1.0); + render(sig->color, range, size); } else { pixmap = QPixmap(); - min_val = -1; - max_val = 1; } } -void Sparkline::render(const QColor &color, QSize size) { - const double xscale = (size.width() - 1) / (double)time_range; +void Sparkline::render(const QColor &color, int range, QSize size) { + const double xscale = (size.width() - 1) / (double)range; const double yscale = (size.height() - 3) / (max_val - min_val); - points.clear(); - points.reserve(values.capacity()); - for (auto &v : values) { - points.emplace_back(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale); + for (auto &v : points) { + v = QPoint(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale); } qreal dpr = qApp->devicePixelRatio(); diff --git a/tools/cabana/chart/sparkline.h b/tools/cabana/chart/sparkline.h index 21cbd40a3f..3bdd8a3ee5 100644 --- a/tools/cabana/chart/sparkline.h +++ b/tools/cabana/chart/sparkline.h @@ -1,29 +1,24 @@ #pragma once -#include - #include #include #include -#include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/dbc/dbc.h" class Sparkline { public: void update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size); - const QSize size() const { return pixmap.size() / pixmap.devicePixelRatio(); } - inline double freq() const { - return values.empty() ? 0 : values.size() / std::max(values.back().x() - values.front().x(), 1.0); - } + inline double freq() const { return freq_; } + bool isEmpty() const { return pixmap.isNull(); } QPixmap pixmap; double min_val = 0; double max_val = 0; - double last_ts = 0; - int time_range = 0; private: - void render(const QColor &color, QSize size); - std::vector values; + void render(const QColor &color, int range, QSize size); + std::vector points; + double freq_ = 0; }; diff --git a/tools/cabana/commands.cc b/tools/cabana/commands.cc index cf48abb4c9..52861723f4 100644 --- a/tools/cabana/commands.cc +++ b/tools/cabana/commands.cc @@ -4,11 +4,13 @@ // EditMsgCommand -EditMsgCommand::EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &comment, QUndoCommand *parent) - : id(id), new_name(name), new_size(size), new_comment(comment), QUndoCommand(parent) { +EditMsgCommand::EditMsgCommand(const MessageId &id, const QString &name, int size, + const QString &node, const QString &comment, QUndoCommand *parent) + : id(id), new_name(name), new_size(size), new_node(node), new_comment(comment), QUndoCommand(parent) { if (auto msg = dbc()->msg(id)) { old_name = msg->name; old_size = msg->size; + old_node = msg->transmitter; old_comment = msg->comment; setText(QObject::tr("edit message %1:%2").arg(name).arg(id.address)); } else { @@ -20,11 +22,11 @@ void EditMsgCommand::undo() { if (old_name.isEmpty()) dbc()->removeMsg(id); else - dbc()->updateMsg(id, old_name, old_size, old_comment); + dbc()->updateMsg(id, old_name, old_size, old_node, old_comment); } void EditMsgCommand::redo() { - dbc()->updateMsg(id, new_name, new_size, new_comment); + dbc()->updateMsg(id, new_name, new_size, new_node, new_comment); } // RemoveMsgCommand @@ -38,7 +40,7 @@ RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : void RemoveMsgCommand::undo() { if (!message.name.isEmpty()) { - dbc()->updateMsg(id, message.name, message.size, message.comment); + dbc()->updateMsg(id, message.name, message.size, message.transmitter, message.comment); for (auto s : message.getSignals()) dbc()->addSignal(id, *s); } @@ -64,7 +66,7 @@ void AddSigCommand::undo() { void AddSigCommand::redo() { if (auto msg = dbc()->msg(id); !msg) { msg_created = true; - dbc()->updateMsg(id, dbc()->newMsgName(id), can->lastMessage(id).dat.size(), ""); + dbc()->updateMsg(id, dbc()->newMsgName(id), can->lastMessage(id).dat.size(), "", ""); } signal.name = dbc()->newSignalName(id); signal.max = std::pow(2, signal.size) - 1; diff --git a/tools/cabana/commands.h b/tools/cabana/commands.h index c7f59c4c7f..0736d9b83f 100644 --- a/tools/cabana/commands.h +++ b/tools/cabana/commands.h @@ -10,13 +10,14 @@ class EditMsgCommand : public QUndoCommand { public: - EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &comment, QUndoCommand *parent = nullptr); + EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &node, + const QString &comment, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: const MessageId id; - QString old_name, new_name, old_comment, new_comment; + QString old_name, new_name, old_comment, new_comment, old_node, new_node; int old_size = 0, new_size = 0; }; diff --git a/tools/cabana/dbc/dbc.cc b/tools/cabana/dbc/dbc.cc index 537749e48d..a0e523d666 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -78,6 +78,9 @@ QString cabana::Msg::newSignalName() { } void cabana::Msg::update() { + if (transmitter.isEmpty()) { + transmitter = DEFAULT_NODE_NAME; + } mask.assign(size, 0x00); multiplexor = nullptr; @@ -125,6 +128,9 @@ void cabana::Msg::update() { void cabana::Signal::update() { updateMsbLsb(*this); + if (receiver_name.isEmpty()) { + receiver_name = DEFAULT_NODE_NAME; + } float h = 19 * (float)lsb / 64.0; h = fmod(h, 1.0); diff --git a/tools/cabana/dbc/dbc.h b/tools/cabana/dbc/dbc.h index e44bc41abf..e07903e680 100644 --- a/tools/cabana/dbc/dbc.h +++ b/tools/cabana/dbc/dbc.h @@ -10,9 +10,9 @@ #include #include -#include "opendbc/can/common_dbc.h" const QString UNTITLED = "untitled"; +const QString DEFAULT_NODE_NAME = "XXX"; struct MessageId { uint8_t source = 0; @@ -119,5 +119,4 @@ public: double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig); void updateMsbLsb(cabana::Signal &s); inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; } -inline std::vector allDBCNames() { return get_dbc_names(); } inline QString doubleToString(double value) { return QString::number(value, 'g', std::numeric_limits::digits10); } diff --git a/tools/cabana/dbc/dbcfile.cc b/tools/cabana/dbc/dbcfile.cc index 2f93c1543e..a22d979212 100644 --- a/tools/cabana/dbc/dbcfile.cc +++ b/tools/cabana/dbc/dbcfile.cc @@ -4,10 +4,8 @@ #include #include #include -#include -#include -DBCFile::DBCFile(const QString &dbc_file_name, QObject *parent) : QObject(parent) { +DBCFile::DBCFile(const QString &dbc_file_name) { QFile file(dbc_file_name); if (file.open(QIODevice::ReadOnly)) { name_ = QFileInfo(dbc_file_name).baseName(); @@ -22,7 +20,7 @@ DBCFile::DBCFile(const QString &dbc_file_name, QObject *parent) : QObject(parent } } -DBCFile::DBCFile(const QString &name, const QString &content, QObject *parent) : QObject(parent), name_(name), filename("") { +DBCFile::DBCFile(const QString &name, const QString &content) : name_(name), filename("") { // Open from clipboard parse(content); } @@ -60,11 +58,12 @@ bool DBCFile::writeContents(const QString &fn) { return false; } -void DBCFile::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment) { +void DBCFile::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment) { auto &m = msgs[id.address]; m.address = id.address; m.name = name; m.size = size; + m.transmitter = node.isEmpty() ? DEFAULT_NODE_NAME : node; m.comment = comment; } @@ -106,7 +105,8 @@ void DBCFile::parse(const QString &content) { int multiplexor_cnt = 0; while (!stream.atEnd()) { ++line_num; - line = stream.readLine().trimmed(); + QString raw_line = stream.readLine(); + line = raw_line.trimmed(); if (line.startsWith("BO_ ")) { multiplexor_cnt = 0; auto match = bo_regexp.match(line); @@ -169,7 +169,7 @@ void DBCFile::parse(const QString &content) { } } else if (line.startsWith("CM_ BO_")) { if (!line.endsWith("\";")) { - int pos = stream.pos() - line.length() - 1; + int pos = stream.pos() - raw_line.length() - 1; line = content.mid(pos, content.indexOf("\";", pos)); } auto match = msg_comment_regexp.match(line); @@ -179,7 +179,7 @@ void DBCFile::parse(const QString &content) { } } else if (line.startsWith("CM_ SG_ ")) { if (!line.endsWith("\";")) { - int pos = stream.pos() - line.length() - 1; + int pos = stream.pos() - raw_line.length() - 1; line = content.mid(pos, content.indexOf("\";", pos)); } auto match = sg_comment_regexp.match(line); @@ -198,7 +198,8 @@ void DBCFile::parse(const QString &content) { QString DBCFile::generateDBC() { QString dbc_string, signal_comment, message_comment, val_desc; for (const auto &[address, m] : msgs) { - dbc_string += QString("BO_ %1 %2: %3 %4\n").arg(address).arg(m.name).arg(m.size).arg(m.transmitter.isEmpty() ? "XXX" : m.transmitter); + const QString transmitter = m.transmitter.isEmpty() ? DEFAULT_NODE_NAME : m.transmitter; + dbc_string += QString("BO_ %1 %2: %3 %4\n").arg(address).arg(m.name).arg(m.size).arg(transmitter); if (!m.comment.isEmpty()) { message_comment += QString("CM_ BO_ %1 \"%2\";\n").arg(address).arg(m.comment); } @@ -221,7 +222,7 @@ QString DBCFile::generateDBC() { .arg(doubleToString(sig->min)) .arg(doubleToString(sig->max)) .arg(sig->unit) - .arg(sig->receiver_name.isEmpty() ? "XXX" : sig->receiver_name); + .arg(sig->receiver_name.isEmpty() ? DEFAULT_NODE_NAME : sig->receiver_name); if (!sig->comment.isEmpty()) { signal_comment += QString("CM_ SG_ %1 %2 \"%3\";\n").arg(address).arg(sig->name).arg(sig->comment); } diff --git a/tools/cabana/dbc/dbcfile.h b/tools/cabana/dbc/dbcfile.h index 78a73d58e4..ade6b249e2 100644 --- a/tools/cabana/dbc/dbcfile.h +++ b/tools/cabana/dbc/dbcfile.h @@ -1,18 +1,15 @@ #pragma once #include -#include #include "tools/cabana/dbc/dbc.h" const QString AUTO_SAVE_EXTENSION = ".tmp"; -class DBCFile : public QObject { - Q_OBJECT - +class DBCFile { public: - DBCFile(const QString &dbc_file_name, QObject *parent=nullptr); - DBCFile(const QString &name, const QString &content, QObject *parent=nullptr); + DBCFile(const QString &dbc_file_name); + DBCFile(const QString &name, const QString &content); ~DBCFile() {} bool save(); @@ -22,7 +19,7 @@ public: void cleanupAutoSaveFile(); QString generateDBC(); - void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment); + void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment); inline void removeMsg(const MessageId &id) { msgs.erase(id.address); } inline const std::map &getMessages() const { return msgs; } diff --git a/tools/cabana/dbc/dbcmanager.cc b/tools/cabana/dbc/dbcmanager.cc index 5736ac1e89..87a7d962c5 100644 --- a/tools/cabana/dbc/dbcmanager.cc +++ b/tools/cabana/dbc/dbcmanager.cc @@ -7,7 +7,7 @@ bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QS try { auto it = std::find_if(dbc_files.begin(), dbc_files.end(), [&](auto &f) { return f.second && f.second->filename == dbc_file_name; }); - auto file = (it != dbc_files.end()) ? it->second : std::make_shared(dbc_file_name, this); + auto file = (it != dbc_files.end()) ? it->second : std::make_shared(dbc_file_name); for (auto s : sources) { dbc_files[s] = file; } @@ -22,7 +22,7 @@ bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QS bool DBCManager::open(const SourceSet &sources, const QString &name, const QString &content, QString *error) { try { - auto file = std::make_shared(name, content, this); + auto file = std::make_shared(name, content); for (auto s : sources) { dbc_files[s] = file; } @@ -82,10 +82,10 @@ void DBCManager::removeSignal(const MessageId &id, const QString &sig_name) { } } -void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment) { +void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment) { auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible - dbc_file->updateMsg(id, name, size, comment); + dbc_file->updateMsg(id, name, size, node, comment); emit msgUpdated(id); } @@ -189,6 +189,13 @@ const SourceSet DBCManager::sources(const DBCFile *dbc_file) const { return sources; } +QString toString(const SourceSet &ss) { + return std::accumulate(ss.cbegin(), ss.cend(), QString(), [](QString str, int source) { + if (!str.isEmpty()) str += ", "; + return str + (source == -1 ? QStringLiteral("all") : QString::number(source)); + }); +} + DBCManager *dbc() { static DBCManager dbc_manager(nullptr); return &dbc_manager; diff --git a/tools/cabana/dbc/dbcmanager.h b/tools/cabana/dbc/dbcmanager.h index 3ac0829487..53a77a2c13 100644 --- a/tools/cabana/dbc/dbcmanager.h +++ b/tools/cabana/dbc/dbcmanager.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -28,7 +29,7 @@ public: void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig); void removeSignal(const MessageId &id, const QString &sig_name); - void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment); + void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment); void removeMsg(const MessageId &id); QString newMsgName(const MessageId &id); @@ -66,15 +67,8 @@ private: DBCManager *dbc(); +QString toString(const SourceSet &ss); inline QString msgName(const MessageId &id) { auto msg = dbc()->msg(id); return msg ? msg->name : UNTITLED; } - -inline QString toString(const SourceSet &ss) { - QStringList ret; - for (auto s : ss) { - ret << (s == -1 ? QString("all") : QString::number(s)); - } - return ret.join(", "); -} diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 2f6e9cbfe8..7befadb722 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -2,7 +2,7 @@ #include #include -#include +#include #include "tools/cabana/commands.h" #include "tools/cabana/mainwin.h" @@ -23,19 +23,15 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart // message title QHBoxLayout *title_layout = new QHBoxLayout(); title_layout->setContentsMargins(3, 6, 3, 0); - time_label = new QLabel(this); - time_label->setToolTip(tr("Current time")); - time_label->setStyleSheet("QLabel{font-weight:bold;}"); - title_layout->addWidget(time_label); - name_label = new ElidedLabel(this); + auto spacer = new QSpacerItem(0, 1); + title_layout->addItem(spacer); + title_layout->addWidget(name_label = new ElidedLabel(this), 1); name_label->setStyleSheet("QLabel{font-weight:bold;}"); name_label->setAlignment(Qt::AlignCenter); - name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - title_layout->addWidget(name_label); auto edit_btn = new ToolButton("pencil", tr("Edit Message")); title_layout->addWidget(edit_btn); - remove_btn = new ToolButton("x-lg", tr("Remove Message")); - title_layout->addWidget(remove_btn); + title_layout->addWidget(remove_btn = new ToolButton("x-lg", tr("Remove Message"))); + spacer->changeSize(edit_btn->sizeHint().width() * 2 + 9, 1); main_layout->addLayout(title_layout); // warning @@ -138,8 +134,11 @@ void DetailWidget::refresh() { } else { warnings.push_back(tr("Drag-Select in binary view to create new signal.")); } + + QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id); + name_label->setText(msg_name); + name_label->setToolTip(msg_name); remove_btn->setEnabled(msg != nullptr); - name_label->setText(msgName(msg_id)); if (!warnings.isEmpty()) { warning_label->setText(warnings.join('\n')); @@ -148,9 +147,8 @@ void DetailWidget::refresh() { warning_widget->setVisible(!warnings.isEmpty()); } -void DetailWidget::updateState(const QHash *msgs) { - time_label->setText(QString::number(can->currentSec(), 'f', 3)); - if ((msgs && !msgs->contains(msg_id))) +void DetailWidget::updateState(const std::set *msgs) { + if ((msgs && !msgs->count(msg_id))) return; if (tab_widget->currentIndex() == 0) @@ -164,8 +162,8 @@ void DetailWidget::editMsg() { int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); EditMessageDialog dlg(msg_id, msgName(msg_id), size, this); if (dlg.exec()) { - UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed(), - dlg.size_spin->value(), dlg.comment_edit->toPlainText().trimmed())); + UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed(), dlg.size_spin->value(), + dlg.node->text().trimmed(), dlg.comment_edit->toPlainText().trimmed())); } } @@ -182,25 +180,24 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit form_layout->addRow("", error_label = new QLabel); error_label->setVisible(false); - name_edit = new QLineEdit(title, this); + form_layout->addRow(tr("Name"), name_edit = new QLineEdit(title, this)); name_edit->setValidator(new NameValidator(name_edit)); - form_layout->addRow(tr("Name"), name_edit); - size_spin = new QSpinBox(this); + form_layout->addRow(tr("Size"), size_spin = new QSpinBox(this)); // TODO: limit the maximum? size_spin->setMinimum(1); size_spin->setValue(size); - form_layout->addRow(tr("Size"), size_spin); + form_layout->addRow(tr("Node"), node = new QLineEdit(this)); + node->setValidator(new NameValidator(name_edit)); form_layout->addRow(tr("Comment"), comment_edit = new QTextEdit(this)); + form_layout->addRow(btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)); + if (auto msg = dbc()->msg(msg_id)) { + node->setText(msg->transmitter); comment_edit->setText(msg->comment); } - - btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); validateName(name_edit->text()); - form_layout->addRow(btn_box); - setFixedWidth(parent->width() * 0.9); connect(name_edit, &QLineEdit::textEdited, this, &EditMessageDialog::validateName); connect(btn_box, &QDialogButtonBox::accepted, this, &QDialog::accept); diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 10c6a6c46e..15e1ee5f2f 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "selfdrive/ui/qt/widgets/controls.h" #include "tools/cabana/binaryview.h" @@ -11,7 +12,6 @@ #include "tools/cabana/historylog.h" #include "tools/cabana/signalview.h" -class MainWindow; class EditMessageDialog : public QDialog { public: EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent); @@ -21,6 +21,7 @@ public: QString original_name; QDialogButtonBox *btn_box; QLineEdit *name_edit; + QLineEdit *node; QTextEdit *comment_edit; QLabel *error_label; QSpinBox *size_spin; @@ -38,10 +39,10 @@ private: void showTabBarContextMenu(const QPoint &pt); void editMsg(); void removeMsg(); - void updateState(const QHash * msgs = nullptr); + void updateState(const std::set *msgs = nullptr); MessageId msg_id; - QLabel *time_label, *warning_icon, *warning_label; + QLabel *warning_icon, *warning_label; ElidedLabel *name_label; QWidget *warning_widget; TabBar *tabbar; diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc index 7381884cf3..b3440b557b 100644 --- a/tools/cabana/historylog.cc +++ b/tools/cabana/historylog.cc @@ -7,7 +7,6 @@ #include #include "tools/cabana/commands.h" -// HistoryLogModel QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { const bool show_signals = display_signals_mode && sigs.size() > 0; @@ -17,11 +16,11 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { return QString::number((m.mono_time / (double)1e9) - can->routeStartTime(), 'f', 2); } int i = index.column() - 1; - return show_signals ? QString::number(m.sig_values[i], 'f', sigs[i]->precision) : toHex(m.data); + return show_signals ? QString::number(m.sig_values[i], 'f', sigs[i]->precision) : QString(); } else if (role == ColorsRole) { - return QVariant::fromValue(m.colors); + return QVariant::fromValue((void *)(&m.colors)); } else if (role == BytesRole) { - return m.data; + return QVariant::fromValue((void *)(&m.data)); } else if (role == Qt::TextAlignmentRole) { return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter); } @@ -123,7 +122,7 @@ void HistoryLogModel::fetchMore(const QModelIndex &parent) { template std::deque HistoryLogModel::fetchData(InputIt first, InputIt last, uint64_t min_time) { std::deque msgs; - QVector values(sigs.size()); + std::vector values(sigs.size()); for (; first != last && (*first)->mono_time > min_time; ++first) { const CanEvent *e = *first; for (int i = 0; i < sigs.size(); ++i) { @@ -132,7 +131,7 @@ std::deque HistoryLogModel::fetchData(InputIt first, I if (!filter_cmp || filter_cmp(values[filter_sig_idx], filter_value)) { auto &m = msgs.emplace_back(); m.mono_time = e->mono_time; - m.data = QByteArray((const char *)e->dat, e->size); + m.data.assign(e->dat, e->dat + e->size); m.sig_values = values; if (msgs.size() >= batch_size && min_time == 0) { return msgs; @@ -146,7 +145,7 @@ std::deque HistoryLogModel::fetchData(uint64_t from_ti const auto &events = can->events(msg_id); const auto freq = can->lastMessage(msg_id).freq; const bool update_colors = !display_signals_mode || sigs.empty(); - + const std::vector no_mask; const auto speed = can->getSpeed(); if (dynamic_mode) { auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) { @@ -155,20 +154,18 @@ std::deque HistoryLogModel::fetchData(uint64_t from_ti auto msgs = fetchData(first, events.rend(), min_time); if (update_colors && (min_time > 0 || messages.empty())) { for (auto it = msgs.rbegin(); it != msgs.rend(); ++it) { - hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, nullptr, freq); + hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, no_mask, freq); it->colors = hex_colors.colors; } } return msgs; } else { assert(min_time == 0); - auto first = std::upper_bound(events.cbegin(), events.cend(), from_time, [](uint64_t ts, auto e) { - return ts < e->mono_time; - }); + auto first = std::upper_bound(events.cbegin(), events.cend(), from_time, CompareCanEvent()); auto msgs = fetchData(first, events.cend(), 0); if (update_colors) { for (auto it = msgs.begin(); it != msgs.end(); ++it) { - hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, nullptr, freq); + hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, no_mask, freq); it->colors = hex_colors.colors; } } @@ -179,7 +176,7 @@ std::deque HistoryLogModel::fetchData(uint64_t from_ti // HeaderView QSize HeaderView::sectionSizeFromContents(int logicalIndex) const { - static QSize time_col_size = fontMetrics().boundingRect({0, 0, 200, 200}, defaultAlignment(), "000000.000").size() + QSize(10, 6); + static const QSize time_col_size = fontMetrics().boundingRect({0, 0, 200, 200}, defaultAlignment(), "000000.000").size() + QSize(10, 6); if (logicalIndex == 0) { return time_col_size; } else { @@ -239,10 +236,11 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { main_layout->addWidget(logs = new QTableView(this)); logs->setModel(model = new HistoryLogModel(this)); delegate = new MessageBytesDelegate(this); - logs->setItemDelegateForColumn(1, new MessageBytesDelegate(this)); logs->setHorizontalHeader(new HeaderView(Qt::Horizontal, this)); logs->horizontalHeader()->setDefaultAlignment(Qt::AlignRight | (Qt::Alignment)Qt::TextWordWrap); logs->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + logs->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + logs->verticalHeader()->setDefaultSectionSize(delegate->sizeForBytes(8).height()); logs->verticalHeader()->setVisible(false); logs->setFrameShape(QFrame::NoFrame); diff --git a/tools/cabana/historylog.h b/tools/cabana/historylog.h index a68fbdbf43..154b139fb0 100644 --- a/tools/cabana/historylog.h +++ b/tools/cabana/historylog.h @@ -46,9 +46,9 @@ public slots: public: struct Message { uint64_t mono_time = 0; - QVector sig_values; - QByteArray data; - QVector colors; + std::vector sig_values; + std::vector data; + std::vector colors; }; template diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index d3caf492a6..f7f2fba45f 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,14 +22,10 @@ #include "tools/cabana/commands.h" #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" - -static MainWindow *main_win = nullptr; -void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - if (type == QtDebugMsg) std::cout << msg.toStdString() << std::endl; - if (main_win) emit main_win->showMessage(msg, 2000); -} +#include "tools/replay/replay.h" MainWindow::MainWindow() : QMainWindow() { + loadFingerprints(); createDockWindows(); setCentralWidget(center_widget = new CenterWidget(this)); createActions(); @@ -45,24 +42,21 @@ MainWindow::MainWindow() : QMainWindow() { } restoreState(settings.window_state); + // install handlers + static auto static_main_win = this; qRegisterMetaType("uint64_t"); qRegisterMetaType("SourceSet"); qRegisterMetaType("ReplyMsgType"); - installMessageHandler([this](ReplyMsgType type, const std::string msg) { - // use queued connection to recv the log messages from replay. - emit showMessage(QString::fromStdString(msg), 2000); + installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) { + emit static_main_win->updateProgressBar(cur, total, success); }); - installDownloadProgressHandler([this](uint64_t cur, uint64_t total, bool success) { - emit updateProgressBar(cur, total, success); + qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) { + if (type == QtDebugMsg) std::cout << msg.toStdString() << std::endl; + emit static_main_win->showMessage(msg, 2000); + }); + installMessageHandler([](ReplyMsgType type, const std::string msg) { + qInfo() << QString::fromStdString(msg); }); - - main_win = this; - qInstallMessageHandler(qLogMessageHandler); - - QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json"); - if (json_file.open(QIODevice::ReadOnly)) { - fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); - } setStyleSheet(QString(R"(QMainWindow::separator { width: %1px; /* when vertical */ @@ -79,6 +73,17 @@ MainWindow::MainWindow() : QMainWindow() { QObject::connect(StreamNotifier::instance(), &StreamNotifier::streamStarted, this, &MainWindow::streamStarted); } +void MainWindow::loadFingerprints() { + QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json"); + if (json_file.open(QIODevice::ReadOnly)) { + fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); + } + // get opendbc names + for (auto fn : QDir(OPENDBC_FILE_PATH).entryList({"*.dbc"}, QDir::Files, QDir::Name)) { + opendbc_names << QFileInfo(fn).baseName(); + } +} + void MainWindow::createActions() { // File menu QMenu *file_menu = menuBar()->addMenu(tr("&File")); @@ -87,8 +92,8 @@ void MainWindow::createActions() { close_stream_act->setEnabled(false); file_menu->addSeparator(); - file_menu->addAction(tr("New DBC File"), [this]() { newFile(); })->setShortcuts(QKeySequence::New); - file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); })->setShortcuts(QKeySequence::Open); + file_menu->addAction(tr("New DBC File"), [this]() { newFile(); }, QKeySequence::New); + file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); }, QKeySequence::Open); manage_dbcs_menu = file_menu->addMenu(tr("Manage &DBC Files")); @@ -104,29 +109,22 @@ void MainWindow::createActions() { file_menu->addSeparator(); QMenu *load_opendbc_menu = file_menu->addMenu(tr("Load DBC from commaai/opendbc")); // load_opendbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }"); - auto dbc_names = allDBCNames(); - std::sort(dbc_names.begin(), dbc_names.end()); - for (const auto &name : dbc_names) { - QString dbc_name = QString::fromStdString(name); - load_opendbc_menu->addAction(dbc_name, [=]() { loadDBCFromOpendbc(dbc_name); }); + for (const auto &dbc_name : opendbc_names) { + load_opendbc_menu->addAction(dbc_name, [this, name = dbc_name]() { loadDBCFromOpendbc(name); }); } file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); }); file_menu->addSeparator(); - save_dbc = file_menu->addAction(tr("Save DBC..."), this, &MainWindow::save); - save_dbc->setShortcuts(QKeySequence::Save); - - save_dbc_as = file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveAs); - save_dbc_as->setShortcuts(QKeySequence::SaveAs); - + save_dbc = file_menu->addAction(tr("Save DBC..."), this, &MainWindow::save, QKeySequence::Save); + save_dbc_as = file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveAs, QKeySequence::SaveAs); copy_dbc_to_clipboard = file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveToClipboard); file_menu->addSeparator(); - file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption)->setShortcuts(QKeySequence::Preferences); + file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption, QKeySequence::Preferences); file_menu->addSeparator(); - file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows)->setShortcuts(QKeySequence::Quit); + file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows, QKeySequence::Quit); // Edit Menu QMenu *edit_menu = menuBar()->addMenu(tr("&Edit")); @@ -160,7 +158,7 @@ void MainWindow::createActions() { // Help Menu QMenu *help_menu = menuBar()->addMenu(tr("&Help")); - help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp)->setShortcuts(QKeySequence::HelpContents); + help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp, QKeySequence::HelpContents); help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); } @@ -377,7 +375,7 @@ void MainWindow::eventsMerged() { auto dbc_name = fingerprint_to_dbc[car_fingerprint]; if (dbc_name != QJsonValue::Undefined) { // Prevent dialog that load autosaved file from blocking replay->start(). - QTimer::singleShot(0, [dbc_name, this]() { loadDBCFromOpendbc(dbc_name.toString()); }); + QTimer::singleShot(0, this, [dbc_name, this]() { loadDBCFromOpendbc(dbc_name.toString()); }); } } } @@ -439,11 +437,11 @@ void MainWindow::saveFile(DBCFile *dbc_file) { if (!dbc_file->filename.isEmpty()) { dbc_file->save(); updateLoadSaveMenus(); + UndoStack::instance()->setClean(); + statusBar()->showMessage(tr("File saved"), 2000); } else if (!dbc_file->isEmpty()) { saveFileAs(dbc_file); } - UndoStack::instance()->setClean(); - statusBar()->showMessage(tr("File saved"), 2000); } void MainWindow::saveFileAs(DBCFile *dbc_file) { @@ -451,6 +449,8 @@ void MainWindow::saveFileAs(DBCFile *dbc_file) { QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); if (!fn.isEmpty()) { dbc_file->saveAs(fn); + UndoStack::instance()->setClean(); + statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000); updateRecentFiles(fn); updateLoadSaveMenus(); } @@ -472,11 +472,7 @@ void MainWindow::saveFileToClipboard(DBCFile *dbc_file) { void MainWindow::updateLoadSaveMenus() { int cnt = dbc()->nonEmptyDBCCount(); - if (cnt > 1) { - save_dbc->setText(tr("Save %1 DBCs...").arg(dbc()->dbcCount())); - } else { - save_dbc->setText(tr("Save DBC...")); - } + save_dbc->setText(cnt > 1 ? tr("Save %1 DBCs...").arg(cnt) : tr("Save DBC...")); save_dbc->setEnabled(cnt > 0); save_dbc_as->setEnabled(cnt == 1); @@ -595,7 +591,9 @@ void MainWindow::closeEvent(QCloseEvent *event) { cleanupAutoSaveFile(); remindSaveChanges(); - main_win = nullptr; + installDownloadProgressHandler(nullptr); + qInstallMessageHandler(nullptr); + if (floating_window) floating_window->deleteLater(); @@ -606,7 +604,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { settings.video_splitter_state = video_splitter->saveState(); } settings.message_header_state = messages_widget->saveHeaderState(); - settings.save(); + QWidget::closeEvent(event); } diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index bbbe8730cb..1a889b88c2 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/dbc/dbcmanager.h" @@ -50,6 +51,7 @@ protected: void saveFile(DBCFile *dbc_file); void saveFileAs(DBCFile *dbc_file); void saveFileToClipboard(DBCFile *dbc_file); + void loadFingerprints(); void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true); void autoSave(); void cleanupAutoSaveFile(); @@ -84,6 +86,7 @@ protected: QProgressBar *progress_bar; QLabel *status_label; QJsonDocument fingerprint_to_dbc; + QStringList opendbc_names; QSplitter *video_splitter = nullptr; enum { MAX_RECENT_FILES = 15 }; QAction *recent_files_acts[MAX_RECENT_FILES] = {}; diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 6a3d11cda1..aba655d66f 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -1,10 +1,9 @@ #include "tools/cabana/messageswidget.h" -#include #include #include -#include +#include #include #include #include @@ -13,34 +12,17 @@ #include "tools/cabana/commands.h" -MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { +MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); - - QHBoxLayout *title_layout = new QHBoxLayout(); - num_msg_label = new QLabel(this); - title_layout->addSpacing(10); - title_layout->addWidget(num_msg_label); - - title_layout->addStretch(); - title_layout->addWidget(multiple_lines_bytes = new QCheckBox(tr("Multiple Lines &Bytes"), this)); - multiple_lines_bytes->setToolTip(tr("Display bytes in multiple lines")); - multiple_lines_bytes->setChecked(settings.multiple_lines_bytes); - QPushButton *clear_filters = new QPushButton(tr("&Clear Filters")); - clear_filters->setEnabled(false); - title_layout->addWidget(clear_filters); - main_layout->addLayout(title_layout); - + main_layout->setSpacing(0); + // toolbar + main_layout->addWidget(createToolBar()); // message table - view = new MessageView(this); - model = new MessageListModel(this); - header = new MessageViewHeader(this); - auto delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes); - - view->setItemDelegate(delegate); - view->setHeader(header); - view->setModel(model); - view->setHeader(header); + main_layout->addWidget(view = new MessageView(this)); + view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_hex)); + view->setModel(model = new MessageListModel(this)); + view->setHeader(header = new MessageViewHeader(this)); view->setSortingEnabled(true); view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder); view->setAllColumnsShowFocus(true); @@ -48,77 +30,54 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { view->setItemsExpandable(false); view->setIndentation(0); view->setRootIsDecorated(false); + view->setUniformRowHeights(!settings.multiple_lines_hex); // Must be called before setting any header parameters to avoid overriding restoreHeaderState(settings.message_header_state); - view->header()->setSectionsMovable(true); - view->header()->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed); - view->header()->setStretchLastSection(true); - - // Header context menu - view->header()->setContextMenuPolicy(Qt::CustomContextMenu); - QObject::connect(view->header(), &QHeaderView::customContextMenuRequested, view, &MessageView::headerContextMenuEvent); - - main_layout->addWidget(view); + header->setSectionsMovable(true); + header->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed); + header->setStretchLastSection(true); + header->setContextMenuPolicy(Qt::CustomContextMenu); // suppress QHBoxLayout *suppress_layout = new QHBoxLayout(); - suppress_add = new QPushButton("Suppress Highlighted"); - suppress_clear = new QPushButton(); - suppress_layout->addWidget(suppress_add); - suppress_layout->addWidget(suppress_clear); - QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Defined Signals"), this); + suppress_layout->addWidget(suppress_add = new QPushButton("Suppress Highlighted")); + suppress_layout->addWidget(suppress_clear = new QPushButton()); + suppress_clear->setToolTip(tr("Clear suppressed")); + suppress_layout->addStretch(1); + QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Signals"), this); + suppress_defined_signals->setToolTip(tr("Suppress defined signals")); suppress_defined_signals->setChecked(settings.suppress_defined_signals); suppress_layout->addWidget(suppress_defined_signals); main_layout->addLayout(suppress_layout); // signals/slots - QObject::connect(header, &MessageViewHeader::filtersUpdated, model, &MessageListModel::setFilterStrings); - QObject::connect(header, &MessageViewHeader::filtersUpdated, [=](const QMap &filters) { - clear_filters->setEnabled(!filters.isEmpty()); - }); + QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow); + QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); - QObject::connect(clear_filters, &QPushButton::clicked, header, &MessageViewHeader::clearFilters); - QObject::connect(multiple_lines_bytes, &QCheckBox::stateChanged, [=](int state) { - settings.multiple_lines_bytes = (state == Qt::Checked); - delegate->setMultipleLines(settings.multiple_lines_bytes); - view->setUniformRowHeights(!settings.multiple_lines_bytes); - - // Reset model to force recalculation of the width of the bytes column - model->forceResetModel(); - }); - QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, [=](int state) { - settings.suppress_defined_signals = (state == Qt::Checked); - emit settings.changed(); - }); + QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MessagesWidget::dbcModified); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &MessagesWidget::dbcModified); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::dbcModified); + QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); } view->updateBytesSectionSize(); + updateTitle(); }); QObject::connect(view->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { - if (current.isValid() && current.row() < model->msgs.size()) { - auto &id = model->msgs[current.row()]; + if (current.isValid() && current.row() < model->items_.size()) { + const auto &id = model->items_[current.row()].id; if (!current_msg_id || id != *current_msg_id) { current_msg_id = id; emit msgSelectionChanged(*current_msg_id); } } }); - QObject::connect(suppress_add, &QPushButton::clicked, [=]() { - model->suppress(); - updateSuppressedButtons(); - }); - QObject::connect(suppress_clear, &QPushButton::clicked, [=]() { - model->clearSuppress(); - updateSuppressedButtons(); - }); - - updateSuppressedButtons(); + QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); + QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); + suppressHighlighted(); setWhatsThis(tr(R"( Message View
@@ -130,26 +89,79 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { )")); } -void MessagesWidget::dbcModified() { - num_msg_label->setText(tr("%1 Messages, %2 Signals").arg(dbc()->msgCount()).arg(dbc()->signalCount())); - model->dbcModified(); +QToolBar *MessagesWidget::createToolBar() { + QToolBar *toolbar = new QToolBar(this); + toolbar->setIconSize({12, 12}); + toolbar->addWidget(num_msg_label = new QLabel(this)); + num_msg_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + auto views_btn = toolbar->addAction(utils::icon("three-dots"), tr("View...")); + views_btn->setMenu(menu); + auto view_button = qobject_cast(toolbar->widgetForAction(views_btn)); + view_button->setPopupMode(QToolButton::InstantPopup); + view_button->setToolButtonStyle(Qt::ToolButtonIconOnly); + view_button->setStyleSheet("QToolButton::menu-indicator { image: none; }"); + return toolbar; +} + +void MessagesWidget::updateTitle() { + auto stats = std::accumulate( + model->items_.begin(), model->items_.end(), std::pair(), + [](const auto &pair, const auto &item) { + auto m = dbc()->msg(item.id); + return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair; + }); + num_msg_label->setText(tr("%1 Messages (%2 DBC Messages, %3 Signals)") + .arg(model->items_.size()).arg(stats.first).arg(stats.second)); } void MessagesWidget::selectMessage(const MessageId &msg_id) { - auto it = std::find(model->msgs.cbegin(), model->msgs.cend(), msg_id); - if (it != model->msgs.cend()) { - view->setCurrentIndex(model->index(std::distance(model->msgs.cbegin(), it), 0)); + auto it = std::find_if(model->items_.cbegin(), model->items_.cend(), + [&msg_id](auto &item) { return item.id == msg_id; }); + if (it != model->items_.cend()) { + view->setCurrentIndex(model->index(std::distance(model->items_.cbegin(), it), 0)); } } -void MessagesWidget::updateSuppressedButtons() { - if (model->suppressed_bytes.empty()) { - suppress_clear->setEnabled(false); - suppress_clear->setText("Clear Suppressed"); - } else { +void MessagesWidget::suppressHighlighted() { + if (sender() == suppress_add) { + size_t n = can->suppressHighlighted(); + suppress_clear->setText(tr("Clear (%1)").arg(n)); suppress_clear->setEnabled(true); - suppress_clear->setText(QString("Clear Suppressed (%1)").arg(model->suppressed_bytes.size())); + } else { + can->clearSuppressed(); + suppress_clear->setText(tr("Clear")); + suppress_clear->setEnabled(false); + } +} + +void MessagesWidget::headerContextMenuEvent(const QPoint &pos) { + menu->exec(header->mapToGlobal(pos)); +} + +void MessagesWidget::menuAboutToShow() { + menu->clear(); + for (int i = 0; i < header->count(); ++i) { + int logical_index = header->logicalIndex(i); + auto action = menu->addAction(model->headerData(logical_index, Qt::Horizontal).toString(), + [=](bool checked) { header->setSectionHidden(logical_index, !checked); }); + action->setCheckable(true); + action->setChecked(!header->isSectionHidden(logical_index)); + // Can't hide the name column + action->setEnabled(logical_index > 0); } + menu->addSeparator(); + auto action = menu->addAction(tr("Mutlti-Line bytes"), this, &MessagesWidget::setMultiLineBytes); + action->setCheckable(true); + action->setChecked(settings.multiple_lines_hex); +} + +void MessagesWidget::setMultiLineBytes(bool multi) { + settings.multiple_lines_hex = multi; + delegate->setMultipleLines(multi); + view->setUniformRowHeights(!multi); + view->updateBytesSectionSize(); + view->doItemsLayout(); } // MessageListModel @@ -160,6 +172,7 @@ QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, case Column::NAME: return tr("Name"); case Column::SOURCE: return tr("Bus"); case Column::ADDRESS: return tr("ID"); + case Column::NODE: return tr("Node"); case Column::FREQ: return tr("Freq"); case Column::COUNT: return tr("Count"); case Column::DATA: return tr("Bytes"); @@ -169,41 +182,35 @@ QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, } QVariant MessageListModel::data(const QModelIndex &index, int role) const { - const auto &id = msgs[index.row()]; - auto &can_data = can->lastMessage(id); + if (!index.isValid() || index.row() >= items_.size()) return {}; - auto getFreq = [](const CanData &d) -> QString { + auto getFreq = [](const CanData &d) { if (d.freq > 0 && (can->currentSec() - d.ts - 1.0 / settings.fps) < (5.0 / d.freq)) { return d.freq >= 0.95 ? QString::number(std::nearbyint(d.freq)) : QString::number(d.freq, 'f', 2); } else { - return "--"; + return QStringLiteral("--"); } }; + const auto &item = items_[index.row()]; + const auto &data = can->lastMessage(item.id); if (role == Qt::DisplayRole) { switch (index.column()) { - case Column::NAME: return msgName(id); - case Column::SOURCE: return id.source != INVALID_SOURCE ? QString::number(id.source) : "N/A"; - case Column::ADDRESS: return QString::number(id.address, 16); - case Column::FREQ: return id.source != INVALID_SOURCE ? getFreq(can_data) : "N/A"; - case Column::COUNT: return id.source != INVALID_SOURCE ? QString::number(can_data.count) : "N/A"; - case Column::DATA: return id.source != INVALID_SOURCE ? toHex(can_data.dat) : "N/A"; + case Column::NAME: return item.name; + case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : "N/A"; + case Column::ADDRESS: return QString::number(item.id.address, 16); + case Column::NODE: return item.node; + case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(data) : "N/A"; + case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(data.count) : "N/A"; + case Column::DATA: return item.id.source != INVALID_SOURCE ? "" : "N/A"; } } else if (role == ColorsRole) { - QVector colors = can_data.colors; - if (!suppressed_bytes.empty()) { - for (int i = 0; i < colors.size(); i++) { - if (suppressed_bytes.contains({id, i})) { - colors[i] = QColor(255, 255, 255, 0); - } - } - } - return QVariant::fromValue(colors); - } else if (role == BytesRole && index.column() == Column::DATA && id.source != INVALID_SOURCE) { - return can_data.dat; + return QVariant::fromValue((void*)(&data.colors)); + } else if (role == BytesRole && index.column() == Column::DATA && item.id.source != INVALID_SOURCE) { + return QVariant::fromValue((void*)(&data.dat)); } else if (role == Qt::ToolTipRole && index.column() == Column::NAME) { - auto msg = dbc()->msg(id); - auto tooltip = msg ? msg->name : UNTITLED; + auto msg = dbc()->msg(item.id); + auto tooltip = item.name; if (msg && !msg->comment.isEmpty()) tooltip += "
" + msg->comment + ""; return tooltip; } @@ -211,49 +218,31 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { } void MessageListModel::setFilterStrings(const QMap &filters) { - filter_str = filters; - fetchData(); + filters_ = filters; + filterAndSort(); } void MessageListModel::dbcModified() { - dbc_address.clear(); + dbc_messages_.clear(); for (const auto &[_, m] : dbc()->getMessages(-1)) { - dbc_address.insert(m.address); + dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address}); } - fetchData(); + filterAndSort(); } -void MessageListModel::sortMessages(std::vector &new_msgs) { - if (sort_column == Column::NAME) { - std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { - auto ll = std::pair{msgName(l), l}; - auto rr = std::pair{msgName(r), r}; - return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; - }); - } else if (sort_column == Column::SOURCE) { - std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { - auto ll = std::pair{l.source, l}; - auto rr = std::pair{r.source, r}; - return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; - }); - } else if (sort_column == Column::ADDRESS) { - std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { - auto ll = std::pair{l.address, l}; - auto rr = std::pair{r.address, r}; - return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; - }); - } else if (sort_column == Column::FREQ) { - std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { - auto ll = std::pair{can->lastMessage(l).freq, l}; - auto rr = std::pair{can->lastMessage(r).freq, r}; - return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; - }); - } else if (sort_column == Column::COUNT) { - std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { - auto ll = std::pair{can->lastMessage(l).count, l}; - auto rr = std::pair{can->lastMessage(r).count, r}; - return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; +void MessageListModel::sortItems(std::vector &items) { + auto do_sort = [order = sort_order](std::vector &m, auto proj) { + std::stable_sort(m.begin(), m.end(), [order, proj = std::move(proj)](auto &l, auto &r) { + return order == Qt::AscendingOrder ? proj(l) < proj(r) : proj(l) > proj(r); }); + }; + switch (sort_column) { + case Column::NAME: do_sort(items, [](auto &item) { return std::tie(item.name, item.id); }); break; + case Column::SOURCE: do_sort(items, [](auto &item) { return std::tie(item.id.source, item.id); }); break; + case Column::ADDRESS: do_sort(items, [](auto &item) { return std::tie(item.id.address, item.id);}); break; + case Column::NODE: do_sort(items, [](auto &item) { return std::tie(item.node, item.id);}); break; + case Column::FREQ: do_sort(items, [](auto &item) { return std::make_pair(can->lastMessage(item.id).freq, item.id); }); break; + case Column::COUNT: do_sort(items, [](auto &item) { return std::make_pair(can->lastMessage(item.id).count, item.id); }); break; } } @@ -272,27 +261,34 @@ static bool parseRange(const QString &filter, uint32_t value, int base = 10) { return ok && value >= min && value <= max; } -bool MessageListModel::matchMessage(const MessageId &id, const CanData &data, const QMap &filters) { +bool MessageListModel::match(const MessageListModel::Item &item) { + if (filters_.isEmpty()) + return true; + bool match = true; - for (auto it = filters.cbegin(); it != filters.cend() && match; ++it) { + const auto &data = can->lastMessage(item.id); + for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) { const QString &txt = it.value(); - QRegularExpression re(txt, QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption); switch (it.key()) { case Column::NAME: { - const auto msg = dbc()->msg(id); - match = re.match(msg ? msg->name : UNTITLED).hasMatch(); - match = match || (msg && std::any_of(msg->sigs.cbegin(), msg->sigs.cend(), - [&re](const auto &s) { return re.match(s->name).hasMatch(); })); + match = item.name.contains(txt, Qt::CaseInsensitive); + if (!match) { + const auto m = dbc()->msg(item.id); + match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(), + [&txt](const auto &s) { return s->name.contains(txt, Qt::CaseInsensitive); }); + } break; } case Column::SOURCE: - match = parseRange(txt, id.source); + match = parseRange(txt, item.id.source); break; - case Column::ADDRESS: { - match = re.match(QString::number(id.address, 16)).hasMatch(); - match = match || parseRange(txt, id.address, 16); + case Column::ADDRESS: + match = QString::number(item.id.address, 16).contains(txt, Qt::CaseInsensitive); + match = match || parseRange(txt, item.id.address, 16); + break; + case Column::NODE: + match = item.node.contains(txt, Qt::CaseInsensitive); break; - } case Column::FREQ: // TODO: Hide stale messages? match = parseRange(txt, data.freq); @@ -300,52 +296,50 @@ bool MessageListModel::matchMessage(const MessageId &id, const CanData &data, co case Column::COUNT: match = parseRange(txt, data.count); break; - case Column::DATA: { - match = QString(data.dat.toHex()).contains(txt, Qt::CaseInsensitive); - match = match || re.match(QString(data.dat.toHex())).hasMatch(); - match = match || re.match(QString(data.dat.toHex(' '))).hasMatch(); + case Column::DATA: + match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive); break; - } } } return match; } -void MessageListModel::fetchData() { - std::vector new_msgs; - new_msgs.reserve(can->last_msgs.size() + dbc_address.size()); - - auto address = dbc_address; - for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) { - if (filter_str.isEmpty() || matchMessage(it.key(), it.value(), filter_str)) { - new_msgs.push_back(it.key()); - } - address.remove(it.key().address); +void MessageListModel::filterAndSort() { + // merge CAN and DBC messages + std::vector all_messages; + all_messages.reserve(can->lastMessages().size() + dbc_messages_.size()); + auto dbc_msgs = dbc_messages_; + for (const auto &[id, m] : can->lastMessages()) { + all_messages.push_back(id); + dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address}); } + all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end()); - // merge all DBC messages - for (auto &addr : address) { - MessageId id{.source = INVALID_SOURCE, .address = addr}; - if (filter_str.isEmpty() || matchMessage(id, {}, filter_str)) { - new_msgs.push_back(id); - } + // filter and sort + std::vector items; + for (const auto &id : all_messages) { + auto msg = dbc()->msg(id); + Item item = {.id = id, + .name = msg ? msg->name : UNTITLED, + .node = msg ? msg->transmitter : QString()}; + if (match(item)) + items.emplace_back(item); } + sortItems(items); - sortMessages(new_msgs); - - if (msgs != new_msgs) { + if (items_ != items) { beginResetModel(); - msgs = std::move(new_msgs); + items_ = std::move(items); endResetModel(); } } -void MessageListModel::msgsReceived(const QHash *new_msgs, bool has_new_ids) { - if (has_new_ids || filter_str.contains(Column::FREQ) || filter_str.contains(Column::COUNT) || filter_str.contains(Column::DATA)) { - fetchData(); +void MessageListModel::msgsReceived(const std::set *new_msgs, bool has_new_ids) { + if (has_new_ids || filters_.contains(Column::FREQ) || filters_.contains(Column::COUNT) || filters_.contains(Column::DATA)) { + filterAndSort(); } - for (int i = 0; i < msgs.size(); ++i) { - if (new_msgs->contains(msgs[i])) { + for (int i = 0; i < items_.size(); ++i) { + if (!new_msgs || new_msgs->count(items_[i].id)) { for (int col = Column::FREQ; col < columnCount(); ++col) emit dataChanged(index(i, col), index(i, col), {Qt::DisplayRole}); } @@ -353,36 +347,13 @@ void MessageListModel::msgsReceived(const QHash *new_msgs, b } void MessageListModel::sort(int column, Qt::SortOrder order) { - if (column != columnCount() - 1) { + if (column != Column::DATA) { sort_column = column; sort_order = order; - fetchData(); + filterAndSort(); } } -void MessageListModel::suppress() { - const double cur_ts = can->currentSec(); - - for (auto &id : msgs) { - auto &can_data = can->lastMessage(id); - for (int i = 0; i < can_data.dat.size(); i++) { - const double dt = cur_ts - can_data.last_change_t[i]; - if (dt < 2.0) { - suppressed_bytes.insert({id, i}); - } - } - } -} - -void MessageListModel::clearSuppress() { - suppressed_bytes.clear(); -} - -void MessageListModel::forceResetModel() { - beginResetModel(); - endResetModel(); -} - // MessageView void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { @@ -413,45 +384,14 @@ void MessageView::updateBytesSectionSize() { auto delegate = ((MessageBytesDelegate *)itemDelegate()); int max_bytes = 8; if (!delegate->multipleLines()) { - for (auto it = can->last_msgs.constBegin(); it != can->last_msgs.constEnd(); ++it) { - max_bytes = std::max(max_bytes, it.value().dat.size()); + for (const auto &[_, m] : can->lastMessages()) { + max_bytes = std::max(max_bytes, m.dat.size()); } } - int width = delegate->widthForBytes(max_bytes); - if (header()->sectionSize(MessageListModel::Column::DATA) != width) { - header()->resizeSection(MessageListModel::Column::DATA, width); - } + header()->resizeSection(MessageListModel::Column::DATA, delegate->sizeForBytes(max_bytes).width()); } -void MessageView::headerContextMenuEvent(const QPoint &pos) { - QMenu *menu = new QMenu(this); - int cur_index = header()->logicalIndexAt(pos); - - QAction *action; - for (int visual_index = 0; visual_index < header()->count(); visual_index++) { - int logical_index = header()->logicalIndex(visual_index); - QString column_name = model()->headerData(logical_index, Qt::Horizontal).toString(); - - // Hide show action - if (header()->isSectionHidden(logical_index)) { - action = menu->addAction(tr("  %1").arg(column_name), [=]() { header()->showSection(logical_index); }); - } else { - action = menu->addAction(tr("✓ %1").arg(column_name), [=]() { header()->hideSection(logical_index); }); - } - - // Can't hide the name column - action->setEnabled(logical_index > 0); - - // Make current column bold - if (logical_index == cur_index) { - QFont font = action->font(); - font.setBold(true); - action->setFont(font); - } - } - - menu->popup(header()->mapToGlobal(pos)); -} +// MessageViewHeader MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizontal, parent) { QObject::connect(this, &QHeaderView::sectionResized, this, &MessageViewHeader::updateHeaderPositions); @@ -461,20 +401,11 @@ MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizont void MessageViewHeader::updateFilters() { QMap filters; for (int i = 0; i < count(); i++) { - if (editors[i]) { - QString filter = editors[i]->text(); - if (!filter.isEmpty()) { - filters[i] = filter; - } + if (editors[i] && !editors[i]->text().isEmpty()) { + filters[i] = editors[i]->text(); } } - emit filtersUpdated(filters); -} - -void MessageViewHeader::clearFilters() { - for (QLineEdit *editor : editors) { - editor->clear(); - } + qobject_cast(model())->setFilterStrings(filters); } void MessageViewHeader::updateHeaderPositions() { @@ -482,8 +413,7 @@ void MessageViewHeader::updateHeaderPositions() { for (int i = 0; i < count(); i++) { if (editors[i]) { int h = editors[i]->sizeHint().height(); - editors[i]->move(sectionViewportPosition(i), sz.height()); - editors[i]->resize(sectionSize(i), h); + editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h); editors[i]->setHidden(isSectionHidden(i)); } } @@ -506,11 +436,7 @@ void MessageViewHeader::updateGeometries() { updateHeaderPositions(); } - QSize MessageViewHeader::sizeHint() const { QSize sz = QHeaderView::sizeHint(); - if (editors[0]) { - sz.setHeight(sz.height() + editors[0]->minimumSizeHint().height() + 1); - } - return sz; + return editors[0] ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz; } diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index f6d71a5a2e..4f54941c64 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -1,17 +1,16 @@ #pragma once #include -#include +#include +#include #include #include -#include -#include #include #include #include #include -#include +#include #include #include "tools/cabana/dbc/dbcmanager.h" @@ -21,11 +20,11 @@ class MessageListModel : public QAbstractTableModel { Q_OBJECT public: - enum Column { NAME = 0, SOURCE, ADDRESS, + NODE, FREQ, COUNT, DATA, @@ -35,24 +34,29 @@ public: QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; - int rowCount(const QModelIndex &parent = QModelIndex()) const override { return msgs.size(); } + int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); } void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; void setFilterStrings(const QMap &filters); - void msgsReceived(const QHash *new_msgs, bool has_new_ids); - void fetchData(); - void suppress(); - void clearSuppress(); - void forceResetModel(); + void msgsReceived(const std::set *new_msgs, bool has_new_ids); + void filterAndSort(); void dbcModified(); - std::vector msgs; - QSet> suppressed_bytes; + + struct Item { + MessageId id; + QString name; + QString node; + bool operator==(const Item &other) const { + return id == other.id && name == other.name && node == other.node; + } + }; + std::vector items_; private: - void sortMessages(std::vector &new_msgs); - bool matchMessage(const MessageId &id, const CanData &data, const QMap &filters); + void sortItems(std::vector &items); + bool match(const MessageListModel::Item &id); - QMap filter_str; - QSet dbc_address; + QMap filters_; + std::set dbc_messages_; int sort_column = 0; Qt::SortOrder sort_order = Qt::AscendingOrder; }; @@ -65,27 +69,16 @@ public: void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {} void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override; void updateBytesSectionSize(); - void headerContextMenuEvent(const QPoint &pos); }; class MessageViewHeader : public QHeaderView { // https://stackoverflow.com/a/44346317 - Q_OBJECT public: MessageViewHeader(QWidget *parent); void updateHeaderPositions(); - void updateGeometries() override; QSize sizeHint() const override; - -public slots: - void clearFilters(); - -signals: - void filtersUpdated(const QMap &filters); - -private: void updateFilters(); QMap editors; @@ -99,21 +92,25 @@ public: void selectMessage(const MessageId &message_id); QByteArray saveHeaderState() const { return view->header()->saveState(); } bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); } - void updateSuppressedButtons(); - -public slots: - void dbcModified(); + void suppressHighlighted(); signals: void msgSelectionChanged(const MessageId &message_id); protected: + QToolBar *createToolBar(); + void headerContextMenuEvent(const QPoint &pos); + void menuAboutToShow(); + void setMultiLineBytes(bool multi); + void updateTitle(); + MessageView *view; MessageViewHeader *header; + MessageBytesDelegate *delegate; std::optional current_msg_id; - QCheckBox *multiple_lines_bytes; MessageListModel *model; QPushButton *suppress_add; QPushButton *suppress_clear; QLabel *num_msg_label; + QMenu *menu; }; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index d0cada680a..17de0a1c0a 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -8,64 +8,49 @@ #include #include #include +#include #include "tools/cabana/util.h" Settings settings; -Settings::Settings() { - load(); +template +void settings_op(SettingOperation op) { + QSettings s("cabana"); + op(s, "absolute_time", settings.absolute_time); + op(s, "fps", settings.fps); + op(s, "max_cached_minutes", settings.max_cached_minutes); + op(s, "chart_height", settings.chart_height); + op(s, "chart_range", settings.chart_range); + op(s, "chart_column_count", settings.chart_column_count); + op(s, "last_dir", settings.last_dir); + op(s, "last_route_dir", settings.last_route_dir); + op(s, "window_state", settings.window_state); + op(s, "geometry", settings.geometry); + op(s, "video_splitter_state", settings.video_splitter_state); + op(s, "recent_files", settings.recent_files); + op(s, "message_header_state", settings.message_header_state); + op(s, "chart_series_type", settings.chart_series_type); + op(s, "theme", settings.theme); + op(s, "sparkline_range", settings.sparkline_range); + op(s, "multiple_lines_hex", settings.multiple_lines_hex); + op(s, "log_livestream", settings.log_livestream); + op(s, "log_path", settings.log_path); + op(s, "drag_direction", (int &)settings.drag_direction); + op(s, "suppress_defined_signals", settings.suppress_defined_signals); } -void Settings::save() { - QSettings s("settings", QSettings::IniFormat); - s.setValue("fps", fps); - s.setValue("max_cached_minutes", max_cached_minutes); - s.setValue("chart_height", chart_height); - s.setValue("chart_range", chart_range); - s.setValue("chart_column_count", chart_column_count); - s.setValue("last_dir", last_dir); - s.setValue("last_route_dir", last_route_dir); - s.setValue("window_state", window_state); - s.setValue("geometry", geometry); - s.setValue("video_splitter_state", video_splitter_state); - s.setValue("recent_files", recent_files); - s.setValue("message_header_state_v3", message_header_state); - s.setValue("chart_series_type", chart_series_type); - s.setValue("theme", theme); - s.setValue("sparkline_range", sparkline_range); - s.setValue("multiple_lines_bytes", multiple_lines_bytes); - s.setValue("log_livestream", log_livestream); - s.setValue("log_path", log_path); - s.setValue("drag_direction", drag_direction); - s.setValue("suppress_defined_signals", suppress_defined_signals); +Settings::Settings() { + last_dir = last_route_dir = QDir::homePath(); + log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/"; + settings_op([](QSettings &s, const QString &key, auto &value) { + if (auto v = s.value(key); v.canConvert>()) + value = v.value>(); + }); } -void Settings::load() { - QSettings s("settings", QSettings::IniFormat); - fps = s.value("fps", 10).toInt(); - max_cached_minutes = s.value("max_cached_minutes", 30).toInt(); - chart_height = s.value("chart_height", 200).toInt(); - chart_range = s.value("chart_range", 3 * 60).toInt(); - chart_column_count = s.value("chart_column_count", 1).toInt(); - last_dir = s.value("last_dir", QDir::homePath()).toString(); - last_route_dir = s.value("last_route_dir", QDir::homePath()).toString(); - window_state = s.value("window_state").toByteArray(); - geometry = s.value("geometry").toByteArray(); - video_splitter_state = s.value("video_splitter_state").toByteArray(); - recent_files = s.value("recent_files").toStringList(); - message_header_state = s.value("message_header_state_v3").toByteArray(); - chart_series_type = s.value("chart_series_type", 0).toInt(); - theme = s.value("theme", 0).toInt(); - sparkline_range = s.value("sparkline_range", 15).toInt(); - multiple_lines_bytes = s.value("multiple_lines_bytes", true).toBool(); - log_livestream = s.value("log_livestream", true).toBool(); - log_path = s.value("log_path").toString(); - drag_direction = (Settings::DragDirection)s.value("drag_direction", 0).toInt(); - suppress_defined_signals = s.value("suppress_defined_signals", false).toBool(); - if (log_path.isEmpty()) { - log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/"; - } +Settings::~Settings() { + settings_op([](QSettings &s, const QString &key, auto &v) { s.setValue(key, v); }); } // SettingsDlg @@ -76,45 +61,39 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { QGroupBox *groupbox = new QGroupBox("General"); QFormLayout *form_layout = new QFormLayout(groupbox); - theme = new QComboBox(this); + form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); theme->setToolTip(tr("You may need to restart cabana after changes theme")); theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); theme->setCurrentIndex(settings.theme); - form_layout->addRow(tr("Color Theme"), theme); - fps = new QSpinBox(this); + form_layout->addRow("FPS", fps = new QSpinBox(this)); fps->setRange(10, 100); fps->setSingleStep(10); fps->setValue(settings.fps); - form_layout->addRow("FPS", fps); - cached_minutes = new QSpinBox(this); + form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); cached_minutes->setRange(5, 60); cached_minutes->setSingleStep(1); cached_minutes->setValue(settings.max_cached_minutes); - form_layout->addRow(tr("Max Cached Minutes"), cached_minutes); main_layout->addWidget(groupbox); groupbox = new QGroupBox("New Signal Settings"); form_layout = new QFormLayout(groupbox); - drag_direction = new QComboBox(this); + form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); drag_direction->setCurrentIndex(settings.drag_direction); - form_layout->addRow(tr("Drag Direction"), drag_direction); main_layout->addWidget(groupbox); groupbox = new QGroupBox("Chart"); form_layout = new QFormLayout(groupbox); - chart_series_type = new QComboBox(this); + form_layout->addRow(tr("Default Series Type"), chart_series_type = new QComboBox(this)); chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")}); chart_series_type->setCurrentIndex(settings.chart_series_type); - form_layout->addRow(tr("Chart Default Series Type"), chart_series_type); - chart_height = new QSpinBox(this); + form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); chart_height->setRange(100, 500); chart_height->setSingleStep(10); chart_height->setValue(settings.chart_height); - form_layout->addRow(tr("Chart Height"), chart_height); main_layout->addWidget(groupbox); log_livestream = new QGroupBox(tr("Enable live stream logging"), this); @@ -126,10 +105,9 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { path_layout->addWidget(browse_btn); main_layout->addWidget(log_livestream); - - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); main_layout->addWidget(buttonBox); - main_layout->addStretch(1); + setFixedSize(400, sizeHint().height()); QObject::connect(browse_btn, &QPushButton::clicked, [this]() { QString fn = QFileDialog::getExistingDirectory( @@ -140,31 +118,22 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { log_path->setText(fn); } }); - QObject::connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton *button) { - auto role = buttonBox->buttonRole(button); - if (role == QDialogButtonBox::AcceptRole) { - save(); - accept(); - } else if (role == QDialogButtonBox::ApplyRole) { - save(); - } else if (role == QDialogButtonBox::RejectRole) { - reject(); - } - }); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); } void SettingsDlg::save() { - settings.fps = fps->value(); if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { // set theme before emit changed utils::setTheme(settings.theme); } + settings.fps = fps->value(); settings.max_cached_minutes = cached_minutes->value(); settings.chart_series_type = chart_series_type->currentIndex(); settings.chart_height = chart_height->value(); settings.log_livestream = log_livestream->isChecked(); settings.log_path = log_path->text(); settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); - settings.save(); emit settings.changed(); + QDialog::accept(); } diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index b8a3797f86..e75c519ac7 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -23,9 +22,9 @@ public: }; Settings(); - void save(); - void load(); + ~Settings(); + bool absolute_time = false; int fps = 10; int max_cached_minutes = 30; int chart_height = 200; @@ -34,7 +33,7 @@ public: int chart_series_type = 0; int theme = 0; int sparkline_range = 15; // 15 seconds - bool multiple_lines_bytes = true; + bool multiple_lines_hex = false; bool log_livestream = true; bool suppress_defined_signals = false; QString log_path; @@ -45,15 +44,13 @@ public: QByteArray window_state; QStringList recent_files; QByteArray message_header_state; - DragDirection drag_direction; + DragDirection drag_direction = MsbFirst; signals: void changed(); }; class SettingsDlg : public QDialog { - Q_OBJECT - public: SettingsDlg(QWidget *parent); void save(); diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 1b1c8dac22..7eb647b650 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -36,8 +36,8 @@ SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(p void SignalModel::insertItem(SignalModel::Item *parent_item, int pos, const cabana::Signal *sig) { Item *item = new Item{.sig = sig, .parent = parent_item, .title = sig->name, .type = Item::Sig}; parent_item->children.insert(pos, item); - QString titles[]{"Name", "Size", "Little Endian", "Signed", "Offset", "Factor", "Type", "Multiplex Value", "Extra Info", - "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Descriptions"}; + QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type", + "Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"}; for (int i = 0; i < std::size(titles); ++i) { item->children.push_back(new Item{.sig = sig, .parent = item, .title = titles[i], .type = (Item::Type)(i + Item::Name)}); } @@ -68,10 +68,7 @@ void SignalModel::refresh() { } SignalModel::Item *SignalModel::getItem(const QModelIndex &index) const { - SignalModel::Item *item = nullptr; - if (index.isValid()) { - item = (SignalModel::Item *)index.internalPointer(); - } + auto item = index.isValid() ? (SignalModel::Item *)index.internalPointer() : nullptr; return item ? item : root.get(); } @@ -134,6 +131,7 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const { case Item::Sig: return item->sig_val; case Item::Name: return item->sig->name; case Item::Size: return item->sig->size; + case Item::Node: return item->sig->receiver_name; case Item::SignalType: return signalTypeToString(item->sig->type); case Item::MultiplexValue: return item->sig->multiplex_value; case Item::Offset: return doubleToString(item->sig->offset); @@ -172,6 +170,7 @@ bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int r switch (item->type) { case Item::Name: s.name = value.toString(); break; case Item::Size: s.size = value.toInt(); break; + case Item::Node: s.receiver_name = value.toString().trimmed(); break; case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break; case Item::MultiplexValue: s.multiplex_value = value.toInt(); break; case Item::Endian: s.is_little_endian = value.toBool(); break; @@ -195,11 +194,11 @@ void SignalModel::showExtraInfo(const QModelIndex &index) { if (item->type == Item::ExtraInfo) { if (!item->parent->extra_expanded) { item->parent->extra_expanded = true; - beginInsertRows(index.parent(), 7, 13); + beginInsertRows(index.parent(), Item::ExtraInfo - 2, Item::Desc - 2); endInsertRows(); } else { item->parent->extra_expanded = false; - beginRemoveRows(index.parent(), 7, 13); + beginRemoveRows(index.parent(), Item::ExtraInfo - 2, Item::Desc - 2); endRemoveRows(); } } @@ -267,6 +266,7 @@ void SignalModel::handleSignalRemoved(const cabana::Signal *sig) { SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { name_validator = new NameValidator(this); + node_validator = new QRegExpValidator(QRegExp("^\\w+(,\\w+)*$"), this); double_validator = new DoubleValidator(this); label_font.setPointSize(8); @@ -351,7 +351,7 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); QRect rect = r.adjusted(sparkline_size.width() + 1, 0, 0, 0); int value_adjust = 10; - if (item->highlight || option.state & QStyle::State_Selected) { + if (!item->sparkline.isEmpty() && (item->highlight || option.state & QStyle::State_Selected)) { painter->drawLine(rect.topLeft(), rect.bottomLeft()); rect.adjust(5, -v_margin, 0, v_margin); painter->setFont(minmax_font); @@ -361,13 +361,12 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->drawText(rect, Qt::AlignLeft | Qt::AlignBottom, min); QFontMetrics fm(minmax_font); value_adjust = std::max(fm.width(min), fm.width(max)) + 5; - } else if (item->sig->type == cabana::Signal::Type::Multiplexed) { + } else if (!item->sparkline.isEmpty() && item->sig->type == cabana::Signal::Type::Multiplexed) { // display freq of multiplexed signal painter->setFont(label_font); QString freq = QString("%1 hz").arg(item->sparkline.freq(), 0, 'g', 2); painter->drawText(rect.adjusted(5, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, freq); - QFontMetrics fm(label_font); - value_adjust = fm.width(freq) + 10; + value_adjust = QFontMetrics(label_font).width(freq) + 10; } // signal value painter->setFont(option.font); @@ -382,20 +381,21 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto item = (SignalModel::Item *)index.internalPointer(); - if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Offset || + if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Node || item->type == SignalModel::Item::Offset || item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::MultiplexValue || item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) { QLineEdit *e = new QLineEdit(parent); e->setFrame(false); - e->setValidator(item->type == SignalModel::Item::Name ? name_validator : double_validator); + if (item->type == SignalModel::Item::Name) e->setValidator(name_validator); + else if (item->type == SignalModel::Item::Node) e->setValidator(node_validator); + else e->setValidator(double_validator); if (item->type == SignalModel::Item::Name) { - QCompleter *completer = new QCompleter(dbc()->signalNames()); + QCompleter *completer = new QCompleter(dbc()->signalNames(), e); completer->setCaseSensitivity(Qt::CaseInsensitive); completer->setFilterMode(Qt::MatchContains); e->setCompleter(completer); } - return e; } else if (item->type == SignalModel::Item::Size) { QSpinBox *spin = new QSpinBox(parent); @@ -443,7 +443,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QRegularExpression re("\\S+"); filter_edit->setValidator(new QRegularExpressionValidator(re, this)); filter_edit->setClearButtonEnabled(true); - filter_edit->setPlaceholderText(tr("filter signals")); + filter_edit->setPlaceholderText(tr("Filter Signal")); hl->addWidget(filter_edit); hl->addStretch(1); @@ -498,6 +498,12 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); + QObject::connect(tree->header(), &QHeaderView::sectionResized, [this](int logicalIndex, int oldSize, int newSize) { + if (logicalIndex == 1) { + value_column_width = newSize; + updateState(); + } + }); setWhatsThis(tr(R"( Signal view
@@ -604,21 +610,17 @@ void SignalView::handleSignalAdded(MessageId id, const cabana::Signal *sig) { } void SignalView::handleSignalUpdated(const cabana::Signal *sig) { - if (int row = model->signalRow(sig); row != -1) { - auto item = model->getItem(model->index(row, 1)); - // invalidate the sparkline - item->sparkline.last_ts = 0; + if (int row = model->signalRow(sig); row != -1) updateState(); - } } -void SignalView::updateState(const QHash *msgs) { +void SignalView::updateState(const std::set *msgs) { const auto &last_msg = can->lastMessage(model->msg_id); - if (model->rowCount() == 0 || (msgs && !msgs->contains(model->msg_id)) || last_msg.dat.size() == 0) return; + if (model->rowCount() == 0 || (msgs && !msgs->count(model->msg_id)) || last_msg.dat.size() == 0) return; for (auto item : model->root->children) { double value = 0; - if (item->sig->getValue((uint8_t *)last_msg.dat.constData(), last_msg.dat.size(), &value)) { + if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) { item->sig_val = item->sig->formatValue(value); } max_value_width = std::max(max_value_width, fontMetrics().width(item->sig_val)); @@ -634,19 +636,16 @@ void SignalView::updateState(const QHash *msgs) { last_visible_row = bottom.parent().isValid() ? bottom.parent().row() : bottom.row(); } - QSize size(tree->columnWidth(1) - delegate->button_size.width(), delegate->button_size.height()); - int min_max_width = std::min(size.width() - 10, QFontMetrics(delegate->minmax_font).width("-000.00") + 5); - int value_width = std::min(max_value_width, size.width() * 0.35); - size -= {value_width + min_max_width, style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2}; - + const static int min_max_width = QFontMetrics(delegate->minmax_font).width("-000.00") + 5; + int available_width = value_column_width - delegate->button_size.width(); + int value_width = std::min(max_value_width + min_max_width, available_width / 2); + QSize size(available_width - value_width, + delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2); QFutureSynchronizer synchronizer; for (int i = first_visible_row; i <= last_visible_row; ++i) { auto item = model->getItem(model->index(i, 1)); - auto &s = item->sparkline; - if (s.last_ts != last_msg.ts || s.size() != size || s.time_range != settings.sparkline_range) { - synchronizer.addFuture(QtConcurrent::run( - &s, &Sparkline::update, model->msg_id, item->sig, last_msg.ts, settings.sparkline_range, size)); - } + synchronizer.addFuture(QtConcurrent::run( + &item->sparkline, &Sparkline::update, model->msg_id, item->sig, last_msg.ts, settings.sparkline_range, size)); } } diff --git a/tools/cabana/signalview.h b/tools/cabana/signalview.h index 9d8571d0b8..30978f928c 100644 --- a/tools/cabana/signalview.h +++ b/tools/cabana/signalview.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -17,7 +18,7 @@ class SignalModel : public QAbstractItemModel { Q_OBJECT public: struct Item { - enum Type {Root, Sig, Name, Size, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc }; + enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc }; ~Item() { qDeleteAll(children); } inline int row() { return parent->children.indexOf(this); } @@ -82,12 +83,12 @@ class SignalItemDelegate : public QStyledItemDelegate { public: SignalItemDelegate(QObject *parent); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - QValidator *name_validator, *double_validator; + QValidator *name_validator, *double_validator, *node_validator; QFont label_font, minmax_font; const int color_label_width = 18; mutable QSize button_size; @@ -117,7 +118,7 @@ private: void setSparklineRange(int value); void handleSignalAdded(MessageId id, const cabana::Signal *sig); void handleSignalUpdated(const cabana::Signal *sig); - void updateState(const QHash *msgs = nullptr); + void updateState(const std::set *msgs = nullptr); struct TreeView : public QTreeView { TreeView(QWidget *parent) : QTreeView(parent) {} @@ -136,6 +137,7 @@ private: } }; int max_value_width = 0; + int value_column_width = 0; TreeView *tree; QLabel *sparkline_label; QSlider *sparkline_range_slider; diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index 8887084578..8a20086b5c 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -1,9 +1,10 @@ #include "tools/cabana/streams/abstractstream.h" #include -#include +#include -#include +#include "common/timing.h" +#include "tools/cabana/settings.h" static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024; // 6MB @@ -16,11 +17,10 @@ StreamNotifier *StreamNotifier::instance() { AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { assert(parent != nullptr); - new_msgs = std::make_unique>(); - event_buffer = std::make_unique(EVENT_NEXT_BUFFER_SIZE); + event_buffer_ = std::make_unique(EVENT_NEXT_BUFFER_SIZE); + QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection); QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); - QObject::connect(&settings, &Settings::changed, this, &AbstractStream::updateMasks); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &AbstractStream::updateMasks); QObject::connect(dbc(), &DBCManager::maskUpdated, this, &AbstractStream::updateMasks); QObject::connect(this, &AbstractStream::streamStarted, [this]() { @@ -32,61 +32,84 @@ AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { } void AbstractStream::updateMasks() { - std::lock_guard lk(mutex); - masks.clear(); - if (settings.suppress_defined_signals) { - for (auto s : sources) { - if (auto f = dbc()->findDBCFile(s)) { - for (const auto &[address, m] : f->getMessages()) { - masks[{.source = (uint8_t)s, .address = address}] = m.mask; - } + std::lock_guard lk(mutex_); + masks_.clear(); + if (!settings.suppress_defined_signals) + return; + + for (const auto s : sources) { + for (const auto &[address, m] : dbc()->getMessages(s)) { + masks_[{.source = (uint8_t)s, .address = address}] = m.mask; + } + } + // clear bit change counts + for (auto &[id, m] : messages_) { + auto &mask = masks_[id]; + const int size = std::min(mask.size(), m.last_changes.size()); + for (int i = 0; i < size; ++i) { + for (int j = 0; j < 8; ++j) { + if (((mask[i] >> (7 - j)) & 1) != 0) m.last_changes[i].bit_change_counts[j] = 0; + } + } + } +} + +void AbstractStream::suppressDefinedSignals(bool suppress) { + settings.suppress_defined_signals = suppress; + updateMasks(); +} + +size_t AbstractStream::suppressHighlighted() { + std::lock_guard lk(mutex_); + size_t cnt = 0; + const double cur_ts = currentSec(); + for (auto &[_, m] : messages_) { + for (auto &last_change : m.last_changes) { + const double dt = cur_ts - last_change.ts; + if (dt < 2.0) { + last_change.suppressed = true; } + // clear bit change counts + last_change.bit_change_counts.fill(0); + cnt += last_change.suppressed; } } + return cnt; } -void AbstractStream::updateMessages(QHash *messages) { +void AbstractStream::clearSuppressed() { + std::lock_guard lk(mutex_); + for (auto &[_, m] : messages_) { + std::for_each(m.last_changes.begin(), m.last_changes.end(), [](auto &c) { c.suppressed = false; }); + } +} + +void AbstractStream::updateLastMessages() { auto prev_src_size = sources.size(); auto prev_msg_size = last_msgs.size(); - for (auto it = messages->begin(); it != messages->end(); ++it) { - const auto &id = it.key(); - last_msgs[id] = it.value(); - sources.insert(id.source); + std::set msgs; + { + std::lock_guard lk(mutex_); + for (const auto &id : new_msgs_) { + const auto &can_data = messages_[id]; + current_sec_ = std::max(current_sec_, can_data.ts); + last_msgs[id] = can_data; + sources.insert(id.source); + } + msgs = std::move(new_msgs_); } + if (sources.size() != prev_src_size) { updateMasks(); emit sourcesUpdated(sources); } - emit updated(); - emit msgsReceived(messages, prev_msg_size != last_msgs.size()); - delete messages; - processing = false; + emit msgsReceived(&msgs, prev_msg_size != last_msgs.size()); } void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) { - std::lock_guard lk(mutex); - auto mask_it = masks.find(id); - std::vector *mask = mask_it == masks.end() ? nullptr : &mask_it->second; - all_msgs[id].compute(id, (const char *)data, size, sec, getSpeed(), mask); - if (!new_msgs->contains(id)) { - new_msgs->insert(id, {}); - } -} - -bool AbstractStream::postEvents() { - // delay posting CAN message if UI thread is busy - if (processing == false) { - processing = true; - for (auto it = new_msgs->begin(); it != new_msgs->end(); ++it) { - it.value() = all_msgs[it.key()]; - } - // use pointer to avoid data copy in queued connection. - QMetaObject::invokeMethod(this, std::bind(&AbstractStream::updateMessages, this, new_msgs.release()), Qt::QueuedConnection); - new_msgs.reset(new QHash); - new_msgs->reserve(100); - return true; - } - return false; + std::lock_guard lk(mutex_); + messages_[id].compute(id, data, size, sec, getSpeed(), masks_[id]); + new_msgs_.insert(id); } const std::vector &AbstractStream::events(const MessageId &id) const { @@ -98,95 +121,82 @@ const std::vector &AbstractStream::events(const MessageId &id) const CanData &AbstractStream::lastMessage(const MessageId &id) { static CanData empty_data = {}; auto it = last_msgs.find(id); - return it != last_msgs.end() ? it.value() : empty_data; + return it != last_msgs.end() ? it->second : empty_data; } // it is thread safe to update data in updateLastMsgsTo. // updateLastMsgsTo is always called in UI thread. void AbstractStream::updateLastMsgsTo(double sec) { - new_msgs.reset(new QHash); - all_msgs.clear(); - last_msgs.clear(); + new_msgs_.clear(); + messages_.clear(); + current_sec_ = sec; uint64_t last_ts = (sec + routeStartTime()) * 1e9; - for (auto &[id, ev] : events_) { - auto it = std::lower_bound(ev.crbegin(), ev.crend(), last_ts, [](auto e, uint64_t ts) { - return e->mono_time > ts; - }); - auto mask_it = masks.find(id); - std::vector *mask = mask_it == masks.end() ? nullptr : &mask_it->second; - if (it != ev.crend()) { - double ts = (*it)->mono_time / 1e9 - routeStartTime(); - auto &m = all_msgs[id]; - m.compute(id, (const char *)(*it)->dat, (*it)->size, ts, getSpeed(), mask); - m.count = std::distance(it, ev.crend()); + for (const auto &[id, ev] : events_) { + auto it = std::upper_bound(ev.begin(), ev.end(), last_ts, CompareCanEvent()); + if (it != ev.begin()) { + auto prev = std::prev(it); + double ts = (*prev)->mono_time / 1e9 - routeStartTime(); + auto &m = messages_[id]; + m.compute(id, (*prev)->dat, (*prev)->size, ts, getSpeed(), {}); + m.count = std::distance(ev.begin(), prev) + 1; } } - // deep copy all_msgs to last_msgs to avoid multi-threading issue. - last_msgs = all_msgs; - last_msgs.detach(); - // use a timer to prevent recursive calls - QTimer::singleShot(0, [this]() { - emit updated(); - emit msgsReceived(&last_msgs, true); - }); + bool id_changed = messages_.size() != last_msgs.size() || + std::any_of(messages_.cbegin(), messages_.cend(), + [this](const auto &m) { return !last_msgs.count(m.first); }); + last_msgs = messages_; + emit msgsReceived(nullptr, id_changed); } -void AbstractStream::mergeEvents(std::vector::const_iterator first, std::vector::const_iterator last) { - static std::unordered_map> new_events_map; - static std::vector new_events; - new_events_map.clear(); - new_events.clear(); - - for (auto it = first; it != last; ++it) { - if ((*it)->which == cereal::Event::Which::CAN) { - uint64_t ts = (*it)->mono_time; - for (const auto &c : (*it)->event.getCan()) { - auto dat = c.getDat(); - CanEvent *e = (CanEvent *)event_buffer->allocate(sizeof(CanEvent) + sizeof(uint8_t) * dat.size()); - e->src = c.getSrc(); - e->address = c.getAddress(); - e->mono_time = ts; - e->size = dat.size(); - memcpy(e->dat, (uint8_t *)dat.begin(), e->size); - - new_events_map[{.source = e->src, .address = e->address}].push_back(e); - new_events.push_back(e); - } - } - } - - auto compare = [](const CanEvent *l, const CanEvent *r) { - return l->mono_time < r->mono_time; - }; +const CanEvent *AbstractStream::newEvent(uint64_t mono_time, const cereal::CanData::Reader &c) { + auto dat = c.getDat(); + CanEvent *e = (CanEvent *)event_buffer_->allocate(sizeof(CanEvent) + sizeof(uint8_t) * dat.size()); + e->src = c.getSrc(); + e->address = c.getAddress(); + e->mono_time = mono_time; + e->size = dat.size(); + memcpy(e->dat, (uint8_t *)dat.begin(), e->size); + return e; +} - for (auto &[id, new_e] : new_events_map) { - auto &e = events_[id]; - auto insert_pos = std::upper_bound(e.cbegin(), e.cend(), new_e.front(), compare); - e.insert(insert_pos, new_e.cbegin(), new_e.cend()); +void AbstractStream::mergeEvents(const std::vector &events) { + static MessageEventsMap msg_events; + std::for_each(msg_events.begin(), msg_events.end(), [](auto &e) { e.second.clear(); }); + for (auto e : events) { + msg_events[{.source = e->src, .address = e->address}].push_back(e); } - auto insert_pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), new_events.front(), compare); - all_events_.insert(insert_pos, new_events.cbegin(), new_events.cend()); - - lastest_event_ts = all_events_.back()->mono_time; - emit eventsMerged(); + if (!events.empty()) { + for (const auto &[id, new_e] : msg_events) { + if (!new_e.empty()) { + auto &e = events_[id]; + auto pos = std::upper_bound(e.cbegin(), e.cend(), new_e.front()->mono_time, CompareCanEvent()); + e.insert(pos, new_e.cbegin(), new_e.cend()); + } + } + auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), events.front()->mono_time, CompareCanEvent()); + all_events_.insert(pos, events.cbegin(), events.cend()); + emit eventsMerged(msg_events); + } + lastest_event_ts = all_events_.empty() ? 0 : all_events_.back()->mono_time; } // CanData namespace { -constexpr int periodic_threshold = 10; -constexpr int start_alpha = 128; -constexpr float fade_time = 2.0; -const QColor CYAN = QColor(0, 187, 255, start_alpha); -const QColor RED = QColor(255, 0, 0, start_alpha); -const QColor GREYISH_BLUE = QColor(102, 86, 169, start_alpha / 2); -const QColor CYAN_LIGHTER = QColor(0, 187, 255, start_alpha).lighter(135); -const QColor RED_LIGHTER = QColor(255, 0, 0, start_alpha).lighter(135); -const QColor GREYISH_BLUE_LIGHTER = QColor(102, 86, 169, start_alpha / 2).lighter(135); +enum Color { GREYISH_BLUE, CYAN, RED}; +QColor getColor(int c) { + constexpr int start_alpha = 128; + static const QColor colors[] = { + [GREYISH_BLUE] = QColor(102, 86, 169, start_alpha / 2), + [CYAN] = QColor(0, 187, 255, start_alpha), + [RED] = QColor(255, 0, 0, start_alpha), + }; + return settings.theme == LIGHT_THEME ? colors[c] : colors[c].lighter(135); +} inline QColor blend(const QColor &a, const QColor &b) { return QColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2); @@ -194,15 +204,11 @@ inline QColor blend(const QColor &a, const QColor &b) { // Calculate the frequency of the past minute. double calc_freq(const MessageId &msg_id, double current_sec) { - auto compare = [](const CanEvent *e, uint64_t mono_time) { - return e->mono_time < mono_time; - }; - const auto &events = can->events(msg_id); uint64_t cur_mono_time = (can->routeStartTime() + current_sec) * 1e9; uint64_t first_mono_time = std::max(0, cur_mono_time - 59 * 1e9); - auto first = std::lower_bound(events.begin(), events.end(), first_mono_time, compare); - auto second = std::lower_bound(first, events.end(), cur_mono_time, compare); + auto first = std::lower_bound(events.begin(), events.end(), first_mono_time, CompareCanEvent()); + auto second = std::lower_bound(first, events.end(), cur_mono_time, CompareCanEvent()); if (first != events.end() && second != events.end()) { double duration = ((*second)->mono_time - (*first)->mono_time) / 1e9; uint32_t count = std::distance(first, second); @@ -213,8 +219,8 @@ double calc_freq(const MessageId &msg_id, double current_sec) { } // namespace -void CanData::compute(const MessageId &msg_id, const char *can_data, const int size, double current_sec, - double playback_speed, const std::vector *mask, double in_freq) { +void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const int size, double current_sec, + double playback_speed, const std::vector &mask, double in_freq) { ts = current_sec; ++count; @@ -225,55 +231,53 @@ void CanData::compute(const MessageId &msg_id, const char *can_data, const int s if (dat.size() != size) { dat.resize(size); - bit_change_counts.resize(size); - colors = QVector(size, QColor(0, 0, 0, 0)); - last_change_t.assign(size, ts); - last_delta.resize(size); - same_delta_counter.resize(size); + colors.assign(size, QColor(0, 0, 0, 0)); + last_changes.resize(size); + std::for_each(last_changes.begin(), last_changes.end(), [current_sec](auto &c) { c.ts = current_sec; }); } else { - bool lighter = settings.theme == DARK_THEME; - const QColor &cyan = !lighter ? CYAN : CYAN_LIGHTER; - const QColor &red = !lighter ? RED : RED_LIGHTER; - const QColor &greyish_blue = !lighter ? GREYISH_BLUE : GREYISH_BLUE_LIGHTER; + constexpr int periodic_threshold = 10; + constexpr float fade_time = 2.0; + const float alpha_delta = 1.0 / (freq + 1) / (fade_time * playback_speed); for (int i = 0; i < size; ++i) { - const uint8_t mask_byte = (mask && i < mask->size()) ? (~((*mask)[i])) : 0xff; + auto &last_change = last_changes[i]; + + uint8_t mask_byte = last_change.suppressed ? 0x00 : 0xFF; + if (i < mask.size()) mask_byte &= ~(mask[i]); + const uint8_t last = dat[i] & mask_byte; const uint8_t cur = can_data[i] & mask_byte; - const int delta = cur - last; - if (last != cur) { - double delta_t = ts - last_change_t[i]; - + const int delta = cur - last; // Keep track if signal is changing randomly, or mostly moving in the same direction - if (std::signbit(delta) == std::signbit(last_delta[i])) { - same_delta_counter[i] = std::min(16, same_delta_counter[i] + 1); + if (std::signbit(delta) == std::signbit(last_change.delta)) { + last_change.same_delta_counter = std::min(16, last_change.same_delta_counter + 1); } else { - same_delta_counter[i] = std::max(0, same_delta_counter[i] - 4); + last_change.same_delta_counter = std::max(0, last_change.same_delta_counter - 4); } + const double delta_t = ts - last_change.ts; // Mostly moves in the same direction, color based on delta up/down - if (delta_t * freq > periodic_threshold || same_delta_counter[i] > 8) { + if (delta_t * freq > periodic_threshold || last_change.same_delta_counter > 8) { // Last change was while ago, choose color based on delta up or down - colors[i] = (cur > last) ? cyan : red; + colors[i] = getColor(cur > last ? CYAN : RED); } else { // Periodic changes - colors[i] = blend(colors[i], greyish_blue); + colors[i] = blend(colors[i], getColor(GREYISH_BLUE)); } // Track bit level changes const uint8_t tmp = (cur ^ last); for (int bit = 0; bit < 8; bit++) { - if (tmp & (1 << bit)) { - bit_change_counts[i][bit] += 1; + if (tmp & (1 << (7 - bit))) { + last_change.bit_change_counts[bit] += 1; } } - last_change_t[i] = ts; - last_delta[i] = delta; + last_change.ts = ts; + last_change.delta = delta; } else { // Fade out - float alpha_delta = 1.0 / (freq + 1) / (fade_time * playback_speed); colors[i].setAlphaF(std::max(0.0, colors[i].alphaF() - alpha_delta)); } } diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 62ab3f2f4b..6c9c025129 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -1,36 +1,38 @@ #pragma once #include -#include -#include #include -#include +#include +#include #include #include #include -#include +#include -#include "common/timing.h" +#include "cereal/messaging/messaging.h" #include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/settings.h" #include "tools/cabana/util.h" -#include "tools/replay/replay.h" struct CanData { - void compute(const MessageId &msg_id, const char *dat, const int size, double current_sec, - double playback_speed, const std::vector *mask = nullptr, double in_freq = 0); + void compute(const MessageId &msg_id, const uint8_t *dat, const int size, double current_sec, + double playback_speed, const std::vector &mask, double in_freq = 0); double ts = 0.; uint32_t count = 0; double freq = 0; - QByteArray dat; - QVector colors; - std::vector last_change_t; - std::vector> bit_change_counts; - std::vector last_delta; - std::vector same_delta_counter; - double last_freq_update_ts = seconds_since_boot(); + std::vector dat; + std::vector colors; + + struct ByteLastChange { + double ts; + int delta; + int same_delta_counter; + bool suppressed; + std::array bit_change_counts; + }; + std::vector last_changes; + double last_freq_update_ts = 0; }; struct CanEvent { @@ -41,12 +43,19 @@ struct CanEvent { uint8_t dat[]; }; +struct CompareCanEvent { + constexpr bool operator()(const CanEvent *const e, uint64_t ts) const { return e->mono_time < ts; } + constexpr bool operator()(uint64_t ts, const CanEvent *const e) const { return ts < e->mono_time; } +}; + struct BusConfig { int can_speed_kbps = 500; int data_speed_kbps = 2000; bool can_fd = false; }; +typedef std::unordered_map> MessageEventsMap; + class AbstractStream : public QObject { Q_OBJECT @@ -54,56 +63,66 @@ public: AbstractStream(QObject *parent); virtual ~AbstractStream() {} virtual void start() = 0; - inline bool liveStreaming() const { return route() == nullptr; } + virtual bool liveStreaming() const { return true; } virtual void seekTo(double ts) {} virtual QString routeName() const = 0; virtual QString carFingerprint() const { return ""; } + virtual QDateTime beginDateTime() const { return {}; } virtual double routeStartTime() const { return 0; } - virtual double currentSec() const = 0; + inline double currentSec() const { return current_sec_; } virtual double totalSeconds() const { return lastEventMonoTime() / 1e9 - routeStartTime(); } - const CanData &lastMessage(const MessageId &id); - virtual VisionStreamType visionStreamType() const { return VISION_STREAM_ROAD; } - virtual const Route *route() const { return nullptr; } virtual void setSpeed(float speed) {} virtual double getSpeed() { return 1; } virtual bool isPaused() const { return false; } virtual void pause(bool pause) {} - const std::vector &allEvents() const { return all_events_; } + + inline const std::unordered_map &lastMessages() const { return last_msgs; } + inline const MessageEventsMap &eventsMap() const { return events_; } + inline const std::vector &allEvents() const { return all_events_; } + const CanData &lastMessage(const MessageId &id); const std::vector &events(const MessageId &id) const; - virtual const std::vector> getTimeline() { return {}; } + + size_t suppressHighlighted(); + void clearSuppressed(); + void suppressDefinedSignals(bool suppress); signals: void paused(); void resume(); void seekedTo(double sec); void streamStarted(); - void eventsMerged(); - void updated(); - void msgsReceived(const QHash *new_msgs, bool has_new_ids); + void eventsMerged(const MessageEventsMap &events_map); + void msgsReceived(const std::set *new_msgs, bool has_new_ids); void sourcesUpdated(const SourceSet &s); + void privateUpdateLastMsgsSignal(); public: - QHash last_msgs; SourceSet sources; protected: - void mergeEvents(std::vector::const_iterator first, std::vector::const_iterator last); - bool postEvents(); - uint64_t lastEventMonoTime() const { return lastest_event_ts; } + void mergeEvents(const std::vector &events); + const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c); void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size); - void updateMessages(QHash *); - void updateMasks(); - void updateLastMsgsTo(double sec); + uint64_t lastEventMonoTime() const { return lastest_event_ts; } - uint64_t lastest_event_ts = 0; - std::atomic processing = false; - std::unique_ptr> new_msgs; - QHash all_msgs; - std::unordered_map> events_; std::vector all_events_; - std::unique_ptr event_buffer; - std::mutex mutex; - std::unordered_map> masks; + uint64_t lastest_event_ts = 0; + +private: + void updateLastMessages(); + void updateLastMsgsTo(double sec); + void updateMasks(); + + double current_sec_ = 0; + MessageEventsMap events_; + std::unordered_map last_msgs; + std::unique_ptr event_buffer_; + + // Members accessed in multiple threads. (mutex protected) + std::mutex mutex_; + std::set new_msgs_; + std::unordered_map messages_; + std::unordered_map> masks_; }; class AbstractOpenStreamWidget : public QWidget { @@ -122,7 +141,6 @@ public: DummyStream(QObject *parent) : AbstractStream(parent) {} QString routeName() const override { return tr("No Stream"); } void start() override { emit streamStarted(); } - double currentSec() const override { return 0; } }; class StreamNotifier : public QObject { diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc index 349a2d7a1c..80507391a7 100644 --- a/tools/cabana/streams/devicestream.cc +++ b/tools/cabana/streams/devicestream.cc @@ -8,6 +8,7 @@ #include #include #include +#include // DeviceStream @@ -21,17 +22,14 @@ void DeviceStream::streamThread() { std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); std::unique_ptr sock(SubSocket::create(context.get(), "can", address)); assert(sock != NULL); - sock->setTimeout(50); // run as fast as messages come in while (!QThread::currentThread()->isInterruptionRequested()) { - Message *msg = sock->receive(true); + std::unique_ptr msg(sock->receive(true)); if (!msg) { QThread::msleep(50); continue; } - - handleEvent(msg->getData(), msg->getSize()); - delete msg; + handleEvent(kj::ArrayPtr((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word))); } } diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index 3f8397f56c..4e1f6d77d9 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -1,12 +1,17 @@ #include "tools/cabana/streams/livestream.h" +#include #include +#include #include +#include "common/timing.h" +#include "common/util.h" + struct LiveStream::Logger { Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {} - void write(const char *data, const size_t size) { + void write(kj::ArrayPtr data) { int n = (seconds_since_epoch() - start_ts) / 60.0; if (std::exchange(segment_num, n) != segment_num) { QString dir = QString("%1/%2--%3") @@ -17,7 +22,8 @@ struct LiveStream::Logger { fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out)); } - fs->write(data, size); + auto bytes = data.asBytes(); + fs->write((const char*)bytes.begin(), bytes.size()); } std::unique_ptr fs; @@ -46,6 +52,7 @@ void LiveStream::start() { emit streamStarted(); stream_thread->start(); startUpdateTimer(); + begin_date_time = QDateTime::currentDateTime(); } LiveStream::~LiveStream() { @@ -56,14 +63,20 @@ LiveStream::~LiveStream() { } // called in streamThread -void LiveStream::handleEvent(const char *data, const size_t size) { +void LiveStream::handleEvent(kj::ArrayPtr data) { if (logger) { - logger->write(data, size); + logger->write(data); } - std::lock_guard lk(lock); - auto &msg = receivedMessages.emplace_back(data, size); - receivedEvents.push_back(msg.event); + capnp::FlatArrayMessageReader reader(data); + auto event = reader.getRoot(); + if (event.which() == cereal::Event::Which::CAN) { + const uint64_t mono_time = event.getLogMonoTime(); + std::lock_guard lk(lock); + for (const auto &c : event.getCan()) { + received_events_.push_back(newEvent(mono_time, c)); + } + } } void LiveStream::timerEvent(QTimerEvent *event) { @@ -71,9 +84,8 @@ void LiveStream::timerEvent(QTimerEvent *event) { { // merge events received from live stream thread. std::lock_guard lk(lock); - mergeEvents(receivedEvents.cbegin(), receivedEvents.cend()); - receivedEvents.clear(); - receivedMessages.clear(); + mergeEvents(received_events_); + received_events_.clear(); } if (!all_events_.empty()) { begin_event_ts = all_events_.front()->mono_time; @@ -102,12 +114,8 @@ void LiveStream::updateEvents() { uint64_t last_ts = post_last_event && speed_ == 1.0 ? all_events_.back()->mono_time : first_event_ts + (nanos_since_boot() - first_update_ts) * speed_; - auto first = std::upper_bound(all_events_.cbegin(), all_events_.cend(), current_event_ts, [](uint64_t ts, auto e) { - return ts < e->mono_time; - }); - auto last = std::upper_bound(first, all_events_.cend(), last_ts, [](uint64_t ts, auto e) { - return ts < e->mono_time; - }); + auto first = std::upper_bound(all_events_.cbegin(), all_events_.cend(), current_event_ts, CompareCanEvent()); + auto last = std::upper_bound(first, all_events_.cend(), last_ts, CompareCanEvent()); for (auto it = first; it != last; ++it) { const CanEvent *e = *it; @@ -115,7 +123,7 @@ void LiveStream::updateEvents() { updateEvent(id, (e->mono_time - begin_event_ts) / 1e9, e->dat, e->size); current_event_ts = e->mono_time; } - postEvents(); + emit privateUpdateLastMsgsSignal(); } void LiveStream::seekTo(double sec) { diff --git a/tools/cabana/streams/livestream.h b/tools/cabana/streams/livestream.h index 38ef2c67f9..7a0f8cd834 100644 --- a/tools/cabana/streams/livestream.h +++ b/tools/cabana/streams/livestream.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -15,8 +14,8 @@ public: LiveStream(QObject *parent); virtual ~LiveStream(); void start() override; + inline QDateTime beginDateTime() const { return begin_date_time; } inline double routeStartTime() const override { return begin_event_ts / 1e9; } - inline double currentSec() const override { return (current_event_ts - begin_event_ts) / 1e9; } void setSpeed(float speed) override { speed_ = speed; } double getSpeed() override { return speed_; } bool isPaused() const override { return paused_; } @@ -25,30 +24,21 @@ public: protected: virtual void streamThread() = 0; - void handleEvent(const char *data, const size_t size); + void handleEvent(kj::ArrayPtr event); private: void startUpdateTimer(); void timerEvent(QTimerEvent *event) override; void updateEvents(); - struct Msg { - Msg(const char *data, const size_t size) { - event = ::new Event(aligned_buf.align(data, size)); - } - ~Msg() { ::delete event; } - Event *event; - AlignedBuffer aligned_buf; - }; - std::mutex lock; QThread *stream_thread; - std::vector receivedEvents; - std::deque receivedMessages; + std::vector received_events_; int timer_id; QBasicTimer update_timer; + QDateTime begin_date_time; uint64_t begin_event_ts = 0; uint64_t current_event_ts = 0; uint64_t first_event_ts = 0; diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index 4a6c588e51..bea1fd7480 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -1,14 +1,13 @@ #include "tools/cabana/streams/pandastream.h" -#include - +#include +#include #include #include #include +#include #include -#include "selfdrive/ui/qt/util.h" - // TODO: remove clearLayout static void clearLayout(QLayout* layout) { while (layout->count() > 0) { @@ -89,7 +88,6 @@ void PandaStream::streamThread() { MessageBuilder msg; auto evt = msg.initEvent(); auto canData = evt.initCan(raw_can_data.size()); - for (uint i = 0; isend_heartbeat(false); } diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h index 43803950f9..919156f400 100644 --- a/tools/cabana/streams/pandastream.h +++ b/tools/cabana/streams/pandastream.h @@ -5,7 +5,6 @@ #include #include -#include #include "tools/cabana/streams/livestream.h" #include "selfdrive/boardd/panda.h" diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 2e580c0f0d..c61c81d56f 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -8,11 +8,14 @@ ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { unsetenv("ZMQ"); + setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1); + // TODO: Remove when OpenpilotPrefix supports ZMQ #ifndef __APPLE__ op_prefix = std::make_unique(); #endif - QObject::connect(&settings, &Settings::changed, [this]() { + + QObject::connect(&settings, &Settings::changed, this, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); }); } @@ -25,18 +28,30 @@ void ReplayStream::mergeSegments() { for (auto &[n, seg] : replay->segments()) { if (seg && seg->isLoaded() && !processed_segments.count(n)) { processed_segments.insert(n); - const auto &events = seg->log->events; - mergeEvents(events.cbegin(), events.cend()); + + std::vector new_events; + new_events.reserve(seg->log->events.size()); + for (auto it = seg->log->events.cbegin(); it != seg->log->events.cend(); ++it) { + if ((*it)->which == cereal::Event::Which::CAN) { + const uint64_t ts = (*it)->mono_time; + for (const auto &c : (*it)->event.getCan()) { + new_events.push_back(newEvent(ts, c)); + } + } + } + mergeEvents(new_events); } } } bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) { - replay.reset(new Replay(route, {"can", "roadEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, {}, nullptr, replay_flags, data_dir, this)); + replay.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"}, + {}, nullptr, replay_flags, data_dir, this)); replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter(event_filter, this); QObject::connect(replay.get(), &Replay::seekedTo, this, &AbstractStream::seekedTo); QObject::connect(replay.get(), &Replay::segmentsMerged, this, &ReplayStream::mergeSegments); + QObject::connect(replay.get(), &Replay::qLogLoaded, this, &ReplayStream::qLogLoaded, Qt::QueuedConnection); return replay->load(); } @@ -47,7 +62,6 @@ void ReplayStream::start() { bool ReplayStream::eventFilter(const Event *event) { static double prev_update_ts = 0; - // delay posting CAN message if UI thread is busy if (event->which == cereal::Event::Which::CAN) { double current_sec = event->mono_time / 1e9 - routeStartTime(); for (const auto &c : event->event.getCan()) { @@ -59,9 +73,8 @@ bool ReplayStream::eventFilter(const Event *event) { double ts = millis_since_boot(); if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { - if (postEvents()) { - prev_update_ts = ts; - } + emit privateUpdateLastMsgsSignal(); + prev_update_ts = ts; } return true; } @@ -80,25 +93,21 @@ AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) { OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { // TODO: get route list from api.comma.ai - QGridLayout *grid_layout = new QGridLayout(); + QGridLayout *grid_layout = new QGridLayout(this); grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route")); auto file_btn = new QPushButton(tr("Browse..."), this); grid_layout->addWidget(file_btn, 0, 2); - grid_layout->addWidget(new QLabel(tr("Video")), 1, 0); - grid_layout->addWidget(choose_video_cb = new QComboBox(this), 1, 1); - QString items[] = {tr("No Video"), tr("Road Camera"), tr("Wide Road Camera"), tr("Driver Camera"), tr("QCamera")}; - for (int i = 0; i < std::size(items); ++i) { - choose_video_cb->addItem(items[i]); - } - choose_video_cb->setCurrentIndex(1); // default is road camera; + grid_layout->addWidget(new QLabel(tr("Camera")), 1, 0); + QHBoxLayout *camera_layout = new QHBoxLayout(); + for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")}) + camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this))); + camera_layout->addStretch(1); + grid_layout->addItem(camera_layout, 1, 1); - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->addLayout(grid_layout); setMinimumWidth(550); - QObject::connect(file_btn, &QPushButton::clicked, [=]() { QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir); if (!dir.isEmpty()) { @@ -120,9 +129,13 @@ bool OpenReplayWidget::open() { if (!is_valid_format) { QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); } else { - uint32_t flags[] = {REPLAY_FLAG_NO_VIPC, REPLAY_FLAG_NONE, REPLAY_FLAG_ECAM, REPLAY_FLAG_DCAM, REPLAY_FLAG_QCAMERA}; auto replay_stream = std::make_unique(qApp); - if (replay_stream->loadRoute(route, data_dir, flags[choose_video_cb->currentIndex()])) { + uint32_t flags = REPLAY_FLAG_NONE; + if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_DCAM; + if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM; + if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; + + if (replay_stream->loadRoute(route, data_dir, flags)) { *stream = replay_stream.release(); } else { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index de69d9e86c..b4e4be4db6 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -1,13 +1,14 @@ #pragma once +#include #include #include #include -#include #include #include "common/prefix.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/replay/replay.h" class ReplayStream : public AbstractStream { Q_OBJECT @@ -18,20 +19,23 @@ public: bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE); bool eventFilter(const Event *event); void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); } + bool liveStreaming() const override { return false; } inline QString routeName() const override { return replay->route()->name(); } inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); } double totalSeconds() const override { return replay->totalSeconds(); } - inline VisionStreamType visionStreamType() const override { return replay->hasFlag(REPLAY_FLAG_ECAM) ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD; } + inline QDateTime beginDateTime() const { return replay->route()->datetime(); } inline double routeStartTime() const override { return replay->routeStartTime() / (double)1e9; } - inline double currentSec() const override { return replay->currentSeconds(); } - inline const Route *route() const override { return replay->route(); } + inline const Route *route() const { return replay->route(); } inline void setSpeed(float speed) override { replay->setSpeed(speed); } inline float getSpeed() const { return replay->getSpeed(); } + inline Replay *getReplay() const { return replay.get(); } inline bool isPaused() const override { return replay->isPaused(); } void pause(bool pause) override; - inline const std::vector> getTimeline() override { return replay->getTimeline(); } static AbstractOpenStreamWidget *widget(AbstractStream **stream); +signals: + void qLogLoaded(int segnum, std::shared_ptr qlog); + private: void mergeSegments(); std::unique_ptr replay = nullptr; @@ -49,5 +53,5 @@ public: private: QLineEdit *route_edit; - QComboBox *choose_video_cb; + std::vector cameras; }; diff --git a/tools/cabana/streams/socketcanstream.cc b/tools/cabana/streams/socketcanstream.cc index 3df8e31f3b..0f13b9901b 100644 --- a/tools/cabana/streams/socketcanstream.cc +++ b/tools/cabana/streams/socketcanstream.cc @@ -1,8 +1,11 @@ #include "tools/cabana/streams/socketcanstream.h" -#include +#include +#include +#include #include #include +#include SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) { if (!available()) { @@ -49,7 +52,6 @@ void SocketCanStream::streamThread() { auto evt = msg.initEvent(); auto canData = evt.initCan(frames.size()); - for (uint i = 0; i < frames.size(); i++) { if (!frames[i].isValid()) continue; @@ -60,8 +62,7 @@ void SocketCanStream::streamThread() { canData[i].setDat(kj::arrayPtr((uint8_t*)payload.data(), payload.size())); } - auto bytes = msg.toBytes(); - handleEvent((const char*)bytes.begin(), bytes.size()); + handleEvent(capnp::messageToFlatArray(msg)); } } diff --git a/tools/cabana/streams/socketcanstream.h b/tools/cabana/streams/socketcanstream.h index 6f2d7aa353..e0fb826acb 100644 --- a/tools/cabana/streams/socketcanstream.h +++ b/tools/cabana/streams/socketcanstream.h @@ -5,10 +5,7 @@ #include #include #include - #include -#include -#include #include "tools/cabana/streams/livestream.h" @@ -21,7 +18,6 @@ class SocketCanStream : public LiveStream { public: SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {}); static AbstractOpenStreamWidget *widget(AbstractStream **stream); - static bool available(); inline QString routeName() const override { diff --git a/tools/cabana/streamselector.cc b/tools/cabana/streamselector.cc index 719ba72920..07755c0fe0 100644 --- a/tools/cabana/streamselector.cc +++ b/tools/cabana/streamselector.cc @@ -2,7 +2,6 @@ #include #include -#include #include #include diff --git a/tools/cabana/tests/test_cabana.cc b/tools/cabana/tests/test_cabana.cc index a3d014dd2c..f6884a2dc9 100644 --- a/tools/cabana/tests/test_cabana.cc +++ b/tools/cabana/tests/test_cabana.cc @@ -1,13 +1,11 @@ -#include "opendbc/can/common.h" #undef INFO #include "catch2/catch.hpp" #include "tools/replay/logreader.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" -// demo route, first segment -const std::string TEST_RLOG_URL = "https://commadata2.blob.core.windows.net/commadata2/a2a0ccea32023010/2023-07-27--13-01-19/0/rlog.bz2"; +const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; TEST_CASE("DBCFile::generateDBC") { QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "tesla_can"); @@ -30,47 +28,7 @@ TEST_CASE("DBCFile::generateDBC") { } } -TEST_CASE("Parse can messages") { - DBCManager dbc(nullptr); - dbc.open({0}, "toyota_new_mc_pt_generated"); - CANParser can_parser(0, "toyota_new_mc_pt_generated", {}, {}); - - LogReader log; - REQUIRE(log.load(TEST_RLOG_URL, nullptr, {}, true)); - REQUIRE(log.events.size() > 0); - for (auto e : log.events) { - if (e->which == cereal::Event::Which::CAN) { - std::map, std::vector> values_1; - for (const auto &c : e->event.getCan()) { - const auto msg = dbc.msg({.source = c.getSrc(), .address = c.getAddress()}); - if (c.getSrc() == 0 && msg) { - for (auto sig : msg->getSignals()) { - double val = get_raw_value((uint8_t *)c.getDat().begin(), c.getDat().size(), *sig); - values_1[{c.getAddress(), sig->name}].push_back(val); - } - } - } - - can_parser.UpdateCans(e->mono_time, e->event.getCan()); - std::vector values_2; - can_parser.query_latest(values_2); - for (auto &[key, v1] : values_1) { - bool found = false; - for (auto &v2 : values_2) { - if (v2.address == key.first && key.second == v2.name.c_str()) { - REQUIRE(v2.all_values.size() == v1.size()); - REQUIRE(v2.all_values == v1); - found = true; - break; - } - } - REQUIRE(found); - } - } - } -} - -TEST_CASE("Parse dbc") { +TEST_CASE("parse_dbc") { QString content = R"( BO_ 160 message_1: 8 EON SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX @@ -84,7 +42,7 @@ VAL_ 160 signal_1 0 "disabled" 1.2 "initializing" 2 "fault"; CM_ BO_ 160 "message comment" ; CM_ SG_ 160 signal_1 "signal comment"; -CM_ SG_ 160 signal_2 "multiple line comment +CM_ SG_ 160 signal_2 "multiple line comment 1 2 ";)"; @@ -114,7 +72,7 @@ CM_ SG_ 160 signal_2 "multiple line comment REQUIRE(sig_1->val_desc[2] == std::pair{2, "fault"}); auto &sig_2 = msg->sigs[1]; - REQUIRE(sig_2->comment == "multiple line comment\n1\n2"); + REQUIRE(sig_2->comment == "multiple line comment \n1\n2"); // multiplexed signals msg = file.msg(162); diff --git a/tools/cabana/tools/findsignal.cc b/tools/cabana/tools/findsignal.cc index b19babdf88..387a912787 100644 --- a/tools/cabana/tools/findsignal.cc +++ b/tools/cabana/tools/findsignal.cc @@ -37,10 +37,10 @@ void FindSignalModel::search(std::function cmp) { filtered_signals.reserve(prev_sigs.size()); QtConcurrent::blockingMap(prev_sigs, [&](auto &s) { const auto &events = can->events(s.id); - auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, [](uint64_t ts, auto &e) { return ts < e->mono_time; }); + auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, CompareCanEvent()); auto last = events.cend(); if (last_time < std::numeric_limits::max()) { - last = std::upper_bound(events.cbegin(), events.cend(), last_time, [](uint64_t ts, auto &e) { return ts < e->mono_time; }); + last = std::upper_bound(events.cbegin(), events.cend(), last_time, CompareCanEvent()); } auto it = std::find_if(first, last, [&](const CanEvent *e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); }); @@ -192,7 +192,7 @@ void FindSignalDlg::search() { search_btn->setEnabled(false); stats_label->setVisible(false); search_btn->setText("Finding ...."); - QTimer::singleShot(0, [=]() { model->search(cmp); }); + QTimer::singleShot(0, this, [=]() { model->search(cmp); }); } void FindSignalDlg::setInitialSignals() { @@ -214,7 +214,9 @@ void FindSignalDlg::setInitialSignals() { sig.factor = factor_edit->text().toDouble(); sig.offset = offset_edit->text().toDouble(); - auto [first_sec, last_sec] = std::minmax(first_time_edit->text().toDouble(), last_time_edit->text().toDouble()); + double first_time_val = first_time_edit->text().toDouble(); + double last_time_val = last_time_edit->text().toDouble(); + auto [first_sec, last_sec] = std::minmax(first_time_val, last_time_val); uint64_t first_time = (can->routeStartTime() + first_sec) * 1e9; model->last_time = std::numeric_limits::max(); if (last_sec > 0) { @@ -222,15 +224,15 @@ void FindSignalDlg::setInitialSignals() { } model->initial_signals.clear(); - for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) { - if (buses.isEmpty() || buses.contains(it.key().source) && (addresses.isEmpty() || addresses.contains(it.key().address))) { - const auto &events = can->events(it.key()); - auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, [](auto e, uint64_t ts) { return e->mono_time < ts; }); + for (const auto &[id, m] : can->lastMessages()) { + if (buses.isEmpty() || buses.contains(id.source) && (addresses.isEmpty() || addresses.contains(id.address))) { + const auto &events = can->events(id); + auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, CompareCanEvent()); if (e != events.cend()) { - const int total_size = it.value().dat.size() * 8; + const int total_size = m.dat.size() * 8; for (int size = min_size->value(); size <= max_size->value(); ++size) { for (int start = 0; start <= total_size - size; ++start) { - FindSignalModel::SearchSignal s{.id = it.key(), .mono_time = first_time, .sig = sig}; + FindSignalModel::SearchSignal s{.id = id, .mono_time = first_time, .sig = sig}; s.sig.start_bit = start; s.sig.size = size; updateMsbLsb(s.sig); diff --git a/tools/cabana/tools/findsignal.h b/tools/cabana/tools/findsignal.h index e9e5f9f180..5ef7461fee 100644 --- a/tools/cabana/tools/findsignal.h +++ b/tools/cabana/tools/findsignal.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include diff --git a/tools/cabana/util.cc b/tools/cabana/util.cc index 951856e32d..f984230c47 100644 --- a/tools/cabana/util.cc +++ b/tools/cabana/util.cc @@ -1,7 +1,6 @@ #include "tools/cabana/util.h" #include -#include #include #include #include @@ -10,7 +9,7 @@ #include #include -#include +#include #include #include #include @@ -20,7 +19,7 @@ // SegmentTree -void SegmentTree::build(const QVector &arr) { +void SegmentTree::build(const std::vector &arr) { size = arr.size(); tree.resize(4 * size); // size of the tree is 4 times the size of the array if (size > 0) { @@ -28,7 +27,7 @@ void SegmentTree::build(const QVector &arr) { } } -void SegmentTree::build_tree(const QVector &arr, int n, int left, int right) { +void SegmentTree::build_tree(const std::vector &arr, int n, int left, int right) { if (left == right) { const double y = arr[left].y(); tree[n] = {y, y}; @@ -56,23 +55,22 @@ std::pair SegmentTree::get_minmax(int n, int left, int right, in MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) : multiple_lines(multiple_lines), QStyledItemDelegate(parent) { fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); + for (int i = 0; i < 256; ++i) { + hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); + hex_text_table[i].prepare({}, fixed_font); + } + h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; + v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; } -int MessageBytesDelegate::widthForBytes(int n) const { - int h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; - return n * byte_size.width() + h_margin * 2; +QSize MessageBytesDelegate::sizeForBytes(int n) const { + int rows = multiple_lines ? std::max(1, n / 8) : 1; + return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; } QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - int v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; auto data = index.data(BytesRole); - if (!data.isValid()) { - return {1, byte_size.height() + 2 * v_margin}; - } - int n = data.toByteArray().size(); - assert(n >= 0 && n <= 64); - return !multiple_lines ? QSize{widthForBytes(n), byte_size.height() + 2 * v_margin} - : QSize{widthForBytes(8), byte_size.height() * std::max(1, n / 8) + 2 * v_margin}; + return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); } void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { @@ -81,20 +79,17 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & return QStyledItemDelegate::paint(painter, option, index); } - auto byte_list = data.toByteArray(); - auto colors = index.data(ColorsRole).value>(); - - int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin); - int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin); + QFont old_font = painter->font(); + QPen old_pen = painter->pen(); if (option.state & QStyle::State_Selected) { painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); } - const QPoint pt{option.rect.left() + h_margin, option.rect.top() + v_margin}; - QFont old_font = painter->font(); - QPen old_pen = painter->pen(); painter->setFont(fixed_font); - for (int i = 0; i < byte_list.size(); ++i) { + + const auto &bytes = *static_cast*>(data.value()); + const auto &colors = *static_cast*>(index.data(ColorsRole).value()); + for (int i = 0; i < bytes.size(); ++i) { int row = !multiple_lines ? 0 : i / 8; int column = !multiple_lines ? i : i % 8; QRect r = QRect({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); @@ -107,7 +102,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & } else if (option.state & QStyle::State_Selected) { painter->setPen(option.palette.color(QPalette::HighlightedText)); } - painter->drawText(r, Qt::AlignCenter, toHex(byte_list[i])); + utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); } painter->setFont(old_font); painter->setPen(old_pen); @@ -239,17 +234,15 @@ void setTheme(int theme) { } } -} // namespace utils - -QString toHex(uint8_t byte) { - static std::array hex = []() { - std::array ret; - for (int i = 0; i < 256; ++i) ret[i] = QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper(); - return ret; - }(); - return hex[byte]; +QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { + QString format = absolute_time ? "yyyy-MM-dd hh:mm:ss" + : (sec > 60 * 60 ? "hh:mm:ss" : "mm:ss"); + if (include_milliseconds) format += ".zzz"; + return QDateTime::fromMSecsSinceEpoch(sec * 1000).toString(format); } +} // namespace utils + int num_decimals(double num) { const QString string = QString::number(num); auto dot_pos = string.indexOf('.'); diff --git a/tools/cabana/util.h b/tools/cabana/util.h index 9e93e74833..158321f784 100644 --- a/tools/cabana/util.h +++ b/tools/cabana/util.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -7,15 +8,15 @@ #include #include -#include #include #include +#include #include #include +#include #include #include #include -#include #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" @@ -54,12 +55,12 @@ enum { class SegmentTree { public: SegmentTree() = default; - void build(const QVector &arr); + void build(const std::vector &arr); inline std::pair minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); } private: std::pair get_minmax(int n, int left, int right, int range_left, int range_right) const; - void build_tree(const QVector &arr, int n, int left, int right); + void build_tree(const std::vector &arr, int n, int left, int right); std::vector> tree; int size = 0; }; @@ -72,17 +73,16 @@ public: QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; bool multipleLines() const { return multiple_lines; } void setMultipleLines(bool v) { multiple_lines = v; } - int widthForBytes(int n) const; + QSize sizeForBytes(int n) const; private: + std::array hex_text_table; QFont fixed_font; QSize byte_size = {}; bool multiple_lines = false; + int h_margin, v_margin; }; -inline QString toHex(const QByteArray &dat) { return dat.toHex(' ').toUpper(); } -QString toHex(uint8_t byte); - class NameValidator : public QRegExpValidator { Q_OBJECT public: @@ -99,9 +99,15 @@ public: namespace utils { QPixmap icon(const QString &id); void setTheme(int theme); -inline QString formatSeconds(int seconds) { - return QDateTime::fromSecsSinceEpoch(seconds, Qt::UTC).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss"); +QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); +inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { + auto size = (r.size() - text.size()) / 2; + p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); +} +inline QString toHex(const std::vector &dat, char separator = '\0') { + return QByteArray::fromRawData((const char *)dat.data(), dat.size()).toHex(separator).toUpper(); } + } class ToolButton : public QToolButton { diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index f535c6b729..a6fd0b2b64 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -1,20 +1,20 @@ #include "tools/cabana/videowidget.h" #include -#include -#include #include -#include -#include +#include +#include +#include #include #include #include #include -#include #include #include +#include "tools/cabana/streams/replaystream.h" + const int MIN_VIDEO_HEIGHT = 100; const int THUMBNAIL_MARGIN = 3; @@ -30,35 +30,16 @@ static const QColor timeline_colors[] = { VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::StyledPanel | QFrame::Plain); auto main_layout = new QVBoxLayout(this); - if (!can->liveStreaming()) { + if (!can->liveStreaming()) main_layout->addWidget(createCameraWidget()); - } + main_layout->addLayout(createPlaybackController()); - // btn controls - QHBoxLayout *control_layout = new QHBoxLayout(); - play_btn = new QPushButton(); - play_btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - control_layout->addWidget(play_btn); - - QButtonGroup *group = new QButtonGroup(this); - group->setExclusive(true); - for (float speed : {0.1, 0.5, 1., 2.}) { - QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this); - btn->setCheckable(true); - QObject::connect(btn, &QPushButton::clicked, [speed]() { can->setSpeed(speed); }); - control_layout->addWidget(btn); - group->addButton(btn); - if (speed == 1.0) btn->setChecked(true); - } - main_layout->addLayout(control_layout); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - - QObject::connect(play_btn, &QPushButton::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); QObject::connect(can, &AbstractStream::resume, this, &VideoWidget::updatePlayBtnState); - QObject::connect(&settings, &Settings::changed, this, &VideoWidget::updatePlayBtnState); - updatePlayBtnState(); + QObject::connect(can, &AbstractStream::msgsReceived, this, &VideoWidget::updateState); + updatePlayBtnState(); setWhatsThis(tr(R"( Video
@@ -81,95 +62,181 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { timeline_colors[(int)TimelineType::AlertCritical].name())); } +QHBoxLayout *VideoWidget::createPlaybackController() { + QHBoxLayout *layout = new QHBoxLayout(); + layout->addWidget(seek_backward_btn = new ToolButton("rewind", tr("Seek backward"))); + layout->addWidget(play_btn = new ToolButton("play", tr("Play"))); + layout->addWidget(seek_forward_btn = new ToolButton("fast-forward", tr("Seek forward"))); + + if (can->liveStreaming()) { + layout->addWidget(skip_to_end_btn = new ToolButton("skip-end", tr("Skip to the end"), this)); + QObject::connect(skip_to_end_btn, &QToolButton::clicked, [this]() { + // set speed to 1.0 + speed_btn->menu()->actions()[7]->setChecked(true); + can->pause(false); + can->seekTo(can->totalSeconds() + 1); + }); + } + + layout->addWidget(time_btn = new QToolButton); + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + time_btn->setAutoRaise(true); + layout->addStretch(0); + + if (!can->liveStreaming()) { + layout->addWidget(loop_btn = new ToolButton("repeat", tr("Loop playback"))); + QObject::connect(loop_btn, &QToolButton::clicked, this, &VideoWidget::loopPlaybackClicked); + } + + // speed selector + layout->addWidget(speed_btn = new QToolButton(this)); + speed_btn->setAutoRaise(true); + speed_btn->setMenu(new QMenu(speed_btn)); + speed_btn->setPopupMode(QToolButton::InstantPopup); + QActionGroup *speed_group = new QActionGroup(this); + speed_group->setExclusive(true); + + int max_width = 0; + QFont font = speed_btn->font(); + font.setBold(true); + speed_btn->setFont(font); + QFontMetrics fm(font); + for (float speed : {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.}) { + QString name = QString("%1x").arg(speed); + max_width = std::max(max_width, fm.width(name) + fm.horizontalAdvance(QLatin1Char(' ')) * 2); + + QAction *act = new QAction(name, speed_group); + act->setCheckable(true); + QObject::connect(act, &QAction::toggled, [this, speed]() { + can->setSpeed(speed); + speed_btn->setText(QString("%1x ").arg(speed)); + }); + speed_btn->menu()->addAction(act); + if (speed == 1.0)act->setChecked(true); + } + speed_btn->setMinimumWidth(max_width + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); + + QObject::connect(play_btn, &QToolButton::clicked, []() { can->pause(!can->isPaused()); }); + QObject::connect(seek_backward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() - 1); }); + QObject::connect(seek_forward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() + 1); }); + QObject::connect(time_btn, &QToolButton::clicked, [this]() { + settings.absolute_time = !settings.absolute_time; + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + updateState(); + }); + return layout; +} + QWidget *VideoWidget::createCameraWidget() { QWidget *w = new QWidget(this); QVBoxLayout *l = new QVBoxLayout(w); l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + + l->addWidget(camera_tab = new TabBar(w)); + camera_tab->setAutoHide(true); + camera_tab->setExpanding(false); QStackedLayout *stacked = new QStackedLayout(); stacked->setStackingMode(QStackedLayout::StackAll); - stacked->addWidget(cam_widget = new CameraWidget("camerad", can->visionStreamType(), false)); + stacked->addWidget(cam_widget = new CameraWidget("camerad", VISION_STREAM_ROAD, false)); cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); stacked->addWidget(alert_label = new InfoLabel(this)); l->addLayout(stacked); - // slider controls - auto slider_layout = new QHBoxLayout(); - slider_layout->addWidget(time_label = new QLabel("00:00")); - - slider = new Slider(this); + l->addWidget(slider = new Slider(w)); slider->setSingleStep(0); - slider_layout->addWidget(slider); - - slider_layout->addWidget(end_time_label = new QLabel(this)); - l->addLayout(slider_layout); setMaximumTime(can->totalSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(utils::formatSeconds(slider->currentSecond())); }); QObject::connect(slider, &Slider::updateMaximumTime, this, &VideoWidget::setMaximumTime, Qt::QueuedConnection); + QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, slider, &Slider::parseQLog); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); - QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); + QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); + QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) { + if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); + }); return w; } +void VideoWidget::vipcAvailableStreamsUpdated(std::set streams) { + static const QString stream_names[] = { + [VISION_STREAM_ROAD] = "Road camera", + [VISION_STREAM_WIDE_ROAD] = "Wide road camera", + [VISION_STREAM_DRIVER] = "Driver camera"}; + + for (int i = 0; i < streams.size(); ++i) { + if (camera_tab->count() <= i) { + camera_tab->addTab(QString()); + } + int type = *std::next(streams.begin(), i); + camera_tab->setTabText(i, stream_names[type]); + camera_tab->setTabData(i, type); + } + while (camera_tab->count() > streams.size()) { + camera_tab->removeTab(camera_tab->count() - 1); + } +} + +void VideoWidget::loopPlaybackClicked() { + auto replay = qobject_cast(can)->getReplay(); + if (!replay) return; + + if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) { + replay->removeFlag(REPLAY_FLAG_NO_LOOP); + loop_btn->setIcon("repeat"); + } else { + replay->addFlag(REPLAY_FLAG_NO_LOOP); + loop_btn->setIcon("repeat-1"); + } +} + void VideoWidget::setMaximumTime(double sec) { maximum_time = sec; - end_time_label->setText(utils::formatSeconds(sec)); slider->setTimeRange(0, sec); } void VideoWidget::updateTimeRange(double min, double max, bool is_zoomed) { - if (can->liveStreaming()) return; - - if (!is_zoomed) { - min = 0; - max = maximum_time; + if (can->liveStreaming()) { + skip_to_end_btn->setEnabled(!is_zoomed); + return; } - end_time_label->setText(utils::formatSeconds(max)); - slider->setTimeRange(min, max); + is_zoomed ? slider->setTimeRange(min, max) + : slider->setTimeRange(0, maximum_time); +} + +QString VideoWidget::formatTime(double sec, bool include_milliseconds) { + if (settings.absolute_time) + sec = can->beginDateTime().addMSecs(sec * 1000).toMSecsSinceEpoch() / 1000.0; + return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time); } void VideoWidget::updateState() { - if (!slider->isSliderDown()) { - slider->setCurrentSecond(can->currentSec()); + if (slider) { + if (!slider->isSliderDown()) + slider->setCurrentSecond(can->currentSec()); + alert_label->showAlert(slider->alertInfo(can->currentSec())); + time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), + formatTime(slider->maximum() / slider->factor))); + } else { + time_btn->setText(formatTime(can->currentSec(), true)); } - alert_label->showAlert(slider->alertInfo(can->currentSec())); } void VideoWidget::updatePlayBtnState() { - play_btn->setIcon(utils::icon(can->isPaused() ? "play" : "pause")); + play_btn->setIcon(can->isPaused() ? "play" : "pause"); play_btn->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); } // Slider -Slider::Slider(QWidget *parent) : thumbnail_label(parent), QSlider(Qt::Horizontal, parent) { +Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { + thumbnail_label = new InfoLabel(parent); setMouseTracking(true); - auto timer = new QTimer(this); - timer->callOnTimeout([this]() { - timeline = can->getTimeline(); - std::sort(timeline.begin(), timeline.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); - update(); - }); - timer->start(2000); - QObject::connect(can, &AbstractStream::eventsMerged, [this]() { - if (!qlog_future) { - qlog_future = std::make_unique>(QtConcurrent::run(this, &Slider::parseQLog)); - } - }); -} - -Slider::~Slider() { - abort_parse_qlog = true; - if (qlog_future) { - qlog_future->waitForFinished(); - } } AlertInfo Slider::alertInfo(double seconds) { - std::lock_guard lk(thumbnail_lock); uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; auto alert_it = alerts.lower_bound(mono_time); bool has_alert = (alert_it != alerts.end()) && ((alert_it->first - mono_time) <= 1e8); @@ -177,7 +244,6 @@ AlertInfo Slider::alertInfo(double seconds) { } QPixmap Slider::thumbnail(double seconds) { - std::lock_guard lk(thumbnail_lock); uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; auto it = thumbnails.lowerBound(mono_time); return it != thumbnails.end() ? it.value() : QPixmap(); @@ -188,36 +254,32 @@ void Slider::setTimeRange(double min, double max) { setRange(min * factor, max * factor); } -void Slider::parseQLog() { - const auto &segments = can->route()->segments(); - for (auto it = segments.rbegin(); it != segments.rend() && !abort_parse_qlog; ++it) { - LogReader log; - std::string qlog = it->second.qlog.toStdString(); - if (!qlog.empty() && log.load(qlog, &abort_parse_qlog, {cereal::Event::Which::THUMBNAIL, cereal::Event::Which::CONTROLS_STATE}, true, 0, 3)) { - if (it == segments.rbegin() && !log.events.empty()) { - double max_time = log.events.back()->mono_time / 1e9 - can->routeStartTime(); - emit updateMaximumTime(max_time); +void Slider::parseQLog(int segnum, std::shared_ptr qlog) { + const auto &segments = qobject_cast(can)->route()->segments(); + if (segments.size() > 0 && segnum == segments.rbegin()->first && !qlog->events.empty()) { + emit updateMaximumTime(qlog->events.back()->mono_time / 1e9 - can->routeStartTime()); + } + + std::mutex mutex; + QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [&mutex, this](const Event *e) { + if (e->which == cereal::Event::Which::THUMBNAIL) { + auto thumb = e->event.getThumbnail(); + auto data = thumb.getThumbnail(); + if (QPixmap pm; pm.loadFromData(data.begin(), data.size(), "jpeg")) { + QPixmap scaled = pm.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); + std::lock_guard lk(mutex); + thumbnails[thumb.getTimestampEof()] = scaled; } - for (auto ev = log.events.cbegin(); ev != log.events.cend() && !abort_parse_qlog; ++ev) { - if ((*ev)->which == cereal::Event::Which::THUMBNAIL) { - auto thumb = (*ev)->event.getThumbnail(); - auto data = thumb.getThumbnail(); - if (QPixmap pm; pm.loadFromData(data.begin(), data.size(), "jpeg")) { - pm = pm.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); - std::lock_guard lk(thumbnail_lock); - thumbnails[thumb.getTimestampEof()] = pm; - } - } else if ((*ev)->which == cereal::Event::Which::CONTROLS_STATE) { - auto cs = (*ev)->event.getControlsState(); - if (cs.getAlertType().size() > 0 && cs.getAlertText1().size() > 0 && - cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE) { - std::lock_guard lk(thumbnail_lock); - alerts.emplace((*ev)->mono_time, AlertInfo{cs.getAlertStatus(), cs.getAlertText1().cStr(), cs.getAlertText2().cStr()}); - } - } + } else if (e->which == cereal::Event::Which::CONTROLS_STATE) { + auto cs = e->event.getControlsState(); + if (cs.getAlertType().size() > 0 && cs.getAlertText1().size() > 0 && + cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE) { + std::lock_guard lk(mutex); + alerts.emplace(e->mono_time, AlertInfo{cs.getAlertStatus(), cs.getAlertText1().cStr(), cs.getAlertText2().cStr()}); } } - } + }); + update(); } void Slider::paintEvent(QPaintEvent *ev) { @@ -227,12 +289,23 @@ void Slider::paintEvent(QPaintEvent *ev) { double min = minimum() / factor; double max = maximum() / factor; - for (auto [begin, end, type] : timeline) { - if (begin > max || end < min) - continue; + auto fillRange = [&](double begin, double end, const QColor &color) { + if (begin > max || end < min) return; r.setLeft(((std::max(min, begin) - min) / (max - min)) * width()); r.setRight(((std::min(max, end) - min) / (max - min)) * width()); - p.fillRect(r, timeline_colors[(int)type]); + p.fillRect(r, color); + }; + + const auto replay = qobject_cast(can)->getReplay(); + for (auto [begin, end, type] : replay->getTimeline()) { + fillRange(begin, end, timeline_colors[(int)type]); + } + + QColor empty_color = palette().color(QPalette::Window); + empty_color.setAlpha(160); + for (const auto &[n, seg] : replay->segments()) { + if (!(seg && seg->isLoaded())) + fillRange(n * 60.0, (n + 1) * 60.0, empty_color); } QStyleOptionSlider opt; @@ -247,8 +320,7 @@ void Slider::paintEvent(QPaintEvent *ev) { void Slider::mousePressEvent(QMouseEvent *e) { QSlider::mousePressEvent(e); if (e->button() == Qt::LeftButton && !isSliderDown()) { - int value = minimum() + ((maximum() - minimum()) * e->x()) / width(); - setValue(value); + setValue(minimum() + ((maximum() - minimum()) * e->x()) / width()); emit sliderReleased(); } } @@ -258,11 +330,11 @@ void Slider::mouseMoveEvent(QMouseEvent *e) { double seconds = (minimum() + pos * ((maximum() - minimum()) / (double)width())) / factor; QPixmap thumb = thumbnail(seconds); if (!thumb.isNull()) { - int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, rect().right() - thumb.width() - THUMBNAIL_MARGIN); - int y = -thumb.height(); - thumbnail_label.showPixmap(mapToParent({x, y}), utils::formatSeconds(seconds), thumb, alertInfo(seconds)); + int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); + int y = -thumb.height() - THUMBNAIL_MARGIN; + thumbnail_label->showPixmap(mapToParent(QPoint(x, y)), utils::formatSeconds(seconds), thumb, alertInfo(seconds)); } else { - thumbnail_label.hide(); + thumbnail_label->hide(); } QSlider::mouseMoveEvent(e); } @@ -274,7 +346,7 @@ bool Slider::event(QEvent *event) { case QEvent::FocusIn: case QEvent::FocusOut: case QEvent::Leave: - thumbnail_label.hide(); + thumbnail_label->hide(); break; default: break; @@ -293,8 +365,7 @@ void InfoLabel::showPixmap(const QPoint &pt, const QString &sec, const QPixmap & second = sec; pixmap = pm; alert_info = alert; - resize(pm.size()); - move(pt); + setGeometry(QRect(pt, pm.size())); setVisible(true); update(); } diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 7ddd315e48..67c2c8a29f 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -1,19 +1,17 @@ #pragma once -#include #include #include -#include -#include -#include +#include -#include -#include -#include +#include +#include #include +#include #include "selfdrive/ui/qt/widgets/cameraview.h" -#include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/util.h" +#include "tools/replay/logreader.h" struct AlertInfo { cereal::ControlsState::AlertStatus status; @@ -37,12 +35,14 @@ class Slider : public QSlider { public: Slider(QWidget *parent); - ~Slider(); double currentSecond() const { return value() / factor; } void setCurrentSecond(double sec) { setValue(sec * factor); } void setTimeRange(double min, double max); AlertInfo alertInfo(double sec); QPixmap thumbnail(double sec); + void parseQLog(int segnum, std::shared_ptr qlog); + + const double factor = 1000.0; signals: void updateMaximumTime(double); @@ -52,16 +52,10 @@ private: void mouseMoveEvent(QMouseEvent *e) override; bool event(QEvent *event) override; void paintEvent(QPaintEvent *ev) override; - void parseQLog(); - const double factor = 1000.0; - std::vector> timeline; - std::mutex thumbnail_lock; - std::atomic abort_parse_qlog = false; QMap thumbnails; std::map alerts; - std::unique_ptr> qlog_future; - InfoLabel thumbnail_label; + InfoLabel *thumbnail_label; }; class VideoWidget : public QFrame { @@ -73,15 +67,24 @@ public: void setMaximumTime(double sec); protected: + QString formatTime(double sec, bool include_milliseconds = false); void updateState(); void updatePlayBtnState(); QWidget *createCameraWidget(); + QHBoxLayout *createPlaybackController(); + void loopPlaybackClicked(); + void vipcAvailableStreamsUpdated(std::set streams); CameraWidget *cam_widget; double maximum_time = 0; - QLabel *end_time_label; - QLabel *time_label; - QPushButton *play_btn; - InfoLabel *alert_label; - Slider *slider; + QToolButton *time_btn = nullptr; + ToolButton *seek_backward_btn = nullptr; + ToolButton *play_btn = nullptr; + ToolButton *seek_forward_btn = nullptr; + ToolButton *loop_btn = nullptr; + QToolButton *speed_btn = nullptr; + ToolButton *skip_to_end_btn = nullptr; + InfoLabel *alert_label = nullptr; + Slider *slider = nullptr; + QTabBar *camera_tab = nullptr; }; diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py index ac7ca2520b..c6e05a0af8 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/tools/camerastream/compressed_vipc.py @@ -19,7 +19,11 @@ ENCODE_SOCKETS = { VisionStreamType.VISION_STREAM_DRIVER: "driverEncodeData", } -def decoder(addr, vipc_server, vst, nvidia, debug=False): +def decoder(addr, vst, nvidia, debug=False): + vipc_server = VisionIpcServer("camerad") + vipc_server.create_buffers(vst, 4, False, W, H) + vipc_server.start_listener() + sock_name = ENCODE_SOCKETS[vst] if debug: print("start decoder for %s" % sock_name) @@ -101,14 +105,9 @@ def decoder(addr, vipc_server, vst, nvidia, debug=False): class CompressedVipc: def __init__(self, addr, vision_streams, nvidia=False, debug=False): - self.vipc_server = VisionIpcServer("camerad") - for vst in vision_streams: - self.vipc_server.create_buffers(vst, 4, False, W, H) - self.vipc_server.start_listener() - self.procs = [] for vst in vision_streams: - p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, nvidia, debug)) + p = multiprocessing.Process(target=decoder, args=(addr, vst, nvidia, debug)) p.start() self.procs.append(p) diff --git a/tools/gpstest/.gitignore b/tools/gpstest/.gitignore deleted file mode 100644 index 992088ef34..0000000000 --- a/tools/gpstest/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -LimeGPS/ -LimeSuite/ -hackrf/ -gps-sdr-sim/ diff --git a/tools/gpstest/README.md b/tools/gpstest/README.md deleted file mode 100644 index 01f44df0ce..0000000000 --- a/tools/gpstest/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# GPS test setup -Testing the GPS receiver using GPS spoofing. At the moment only -static location relpay is supported. - -# Usage -on C3 run `rpc_server.py`, on host PC run `fuzzy_testing.py` - -`simulate_gps_signal.py` downloads the latest ephemeris file from -https://cddis.nasa.gov/archive/gnss/data/daily/20xx/brdc/. - - -# Hardware Setup -* [LimeSDR USB](https://wiki.myriadrf.org/LimeSDR-USB) -* Asus AX58BT antenna - -# Software Setup -* https://github.com/myriadrf/LimeSuite -To communicate with LimeSDR the LimeSuite is needed it abstracts the direct -communication. It also contains examples for a quick start. - -The latest stable version (22.09) does not have the corresponding firmware -download available at https://downloads.myriadrf.org/project/limesuite. Therefore -version 20.10 was chosen. - -* https://github.com/osqzss/LimeGPS -Built on top of LimeSuite (libLimeSuite.so.20.10-1), generates the GPS signal. - -``` -./LimeGPS -e -l - -# Example -./LimeGPS -e /pathTo/brdc2660.22n -l 47.202028,15.740394,100 -``` diff --git a/tools/gpstest/fuzzy_testing.py b/tools/gpstest/fuzzy_testing.py deleted file mode 100755 index 3bad2770cd..0000000000 --- a/tools/gpstest/fuzzy_testing.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import multiprocessing -import rpyc -from collections import defaultdict - -from helper import download_rinex, exec_LimeGPS_bin -from helper import get_random_coords, get_continuous_coords - -#------------------------------------------------------------------------------ -# this script is supposed to run on HOST PC -# limeSDR is unreliable via c3 USB -#------------------------------------------------------------------------------ - - -def run_lime_gps(rinex_file: str, location: str, timeout: int): - # needs to run longer than the checker - timeout += 10 - print(f"LimeGPS {location} {timeout}") - p = multiprocessing.Process(target=exec_LimeGPS_bin, - args=(rinex_file, location, timeout)) - p.start() - return p - -con = None -def run_remote_checker(lat, lon, alt, duration, ip_addr): - global con - try: - con = rpyc.connect(ip_addr, 18861) - con._config['sync_request_timeout'] = duration+20 - except ConnectionRefusedError: - print("could not run remote checker is 'rpc_server.py' running???") - return False, None, None - - matched, log, info = con.root.exposed_run_checker(lat, lon, alt, - timeout=duration, - use_laikad=True) - con.close() # TODO: might wanna fetch more logs here - con = None - - print(f"Remote Checker: {log} {info}") - return matched, log, info - - -stats = defaultdict(int) # type: ignore -keys = ['success', 'failed', 'ublox_fail', 'laikad_fail', 'proc_crash', 'checker_crash'] - -def print_report(): - print("\nFuzzy testing report summary:") - for k in keys: - print(f" {k}: {stats[k]}") - - -def update_stats(matched, log, info): - if matched: - stats['success'] += 1 - return - - stats['failed'] += 1 - if log == "PROC CRASH": - stats['proc_crash'] += 1 - if log == "CHECKER CRASHED": - stats['checker_crash'] += 1 - if log == "TIMEOUT": - if "LAIKAD" in info: - stats['laikad_fail'] += 1 - else: # "UBLOX" in info - stats['ublox_fail'] += 1 - - -def main(ip_addr, continuous_mode, timeout, pos): - rinex_file = download_rinex() - - lat, lon, alt = pos - if lat == 0 and lon == 0 and alt == 0: - lat, lon, alt = get_random_coords(47.2020, 15.7403) - - try: - while True: - # spoof random location - spoof_proc = run_lime_gps(rinex_file, f"{lat},{lon},{alt}", timeout) - - # remote checker execs blocking - matched, log, info = run_remote_checker(lat, lon, alt, timeout, ip_addr) - update_stats(matched, log, info) - spoof_proc.terminate() - spoof_proc = None - - if continuous_mode: - lat, lon, alt = get_continuous_coords(lat, lon, alt) - else: - lat, lon, alt = get_random_coords(lat, lon) - except KeyboardInterrupt: - if spoof_proc is not None: - spoof_proc.terminate() - - if con is not None and not con.closed: - con.root.exposed_kill_procs() - con.close() - - print_report() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Fuzzy test GPS stack with random locations.") - parser.add_argument("ip_addr", type=str) - parser.add_argument("-c", "--contin", type=bool, nargs='?', default=False, help='Continous location change') - parser.add_argument("-t", "--timeout", type=int, nargs='?', default=180, help='Timeout to get location') - - # for replaying a location - parser.add_argument("lat", type=float, nargs='?', default=0) - parser.add_argument("lon", type=float, nargs='?', default=0) - parser.add_argument("alt", type=float, nargs='?', default=0) - args = parser.parse_args() - main(args.ip_addr, args.contin, args.timeout, (args.lat, args.lon, args.alt)) diff --git a/tools/gpstest/helper.py b/tools/gpstest/helper.py deleted file mode 100644 index 4f62e60db0..0000000000 --- a/tools/gpstest/helper.py +++ /dev/null @@ -1,53 +0,0 @@ -import random -import datetime as dt -import subprocess as sp -from typing import Tuple - -from laika.downloader import download_nav -from laika.gps_time import GPSTime -from laika.helpers import ConstellationId - - -def download_rinex(): - # TODO: check if there is a better way to get the full brdc file for LimeGPS - gps_time = GPSTime.from_datetime(dt.datetime.utcnow()) - utc_time = dt.datetime.utcnow() - dt.timedelta(1) - gps_time = GPSTime.from_datetime(dt.datetime(utc_time.year, utc_time.month, utc_time.day)) - return download_nav(gps_time, '/tmp/gpstest/', ConstellationId.GPS) - - -def exec_LimeGPS_bin(rinex_file: str, location: str, duration: int): - # this functions should never return, cause return means, timeout is - # reached or it crashed - try: - cmd = ["LimeGPS/LimeGPS", "-e", rinex_file, "-l", location] - sp.check_output(cmd, timeout=duration) - except sp.TimeoutExpired: - print("LimeGPS timeout reached!") - except Exception as e: - print(f"LimeGPS crashed: {str(e)}") - - -def get_random_coords(lat, lon) -> Tuple[float, float, int]: - # jump around the world - # max values, lat: -90 to 90, lon: -180 to 180 - - lat_add = random.random()*20 + 10 - lon_add = random.random()*20 + 20 - alt = random.randint(-10**3, 4*10**3) - - lat = ((lat + lat_add + 90) % 180) - 90 - lon = ((lon + lon_add + 180) % 360) - 180 - return round(lat, 5), round(lon, 5), alt - - -def get_continuous_coords(lat, lon, alt) -> Tuple[float, float, int]: - # continuously move around the world - lat_add = random.random()*0.01 - lon_add = random.random()*0.01 - alt_add = random.randint(-100, 100) - - lat = ((lat + lat_add + 90) % 180) - 90 - lon = ((lon + lon_add + 180) % 360) - 180 - alt += alt_add - return round(lat, 5), round(lon, 5), alt diff --git a/tools/gpstest/patches/hackrf.patch b/tools/gpstest/patches/hackrf.patch deleted file mode 100644 index afc9ac437b..0000000000 --- a/tools/gpstest/patches/hackrf.patch +++ /dev/null @@ -1,44 +0,0 @@ -diff --git a/host/hackrf-tools/src/CMakeLists.txt b/host/hackrf-tools/src/CMakeLists.txt -index 7115151c..a51388ba 100644 ---- a/host/hackrf-tools/src/CMakeLists.txt -+++ b/host/hackrf-tools/src/CMakeLists.txt -@@ -23,20 +23,20 @@ - - set(INSTALL_DEFAULT_BINDIR "bin" CACHE STRING "Appended to CMAKE_INSTALL_PREFIX") - --find_package(FFTW REQUIRED) --include_directories(${FFTW_INCLUDES}) --get_filename_component(FFTW_LIBRARY_DIRS ${FFTW_LIBRARIES} DIRECTORY) --link_directories(${FFTW_LIBRARY_DIRS}) -+#find_package(FFTW REQUIRED) -+#include_directories(${FFTW_INCLUDES}) -+#get_filename_component(FFTW_LIBRARY_DIRS ${FFTW_LIBRARIES} DIRECTORY) -+#link_directories(${FFTW_LIBRARY_DIRS}) - - SET(TOOLS - hackrf_transfer -- hackrf_spiflash -- hackrf_cpldjtag -+ #hackrf_spiflash -+ #hackrf_cpldjtag - hackrf_info -- hackrf_debug -- hackrf_clock -- hackrf_sweep -- hackrf_operacake -+ #hackrf_debug -+ #hackrf_clock -+ #hackrf_sweep -+ #hackrf_operacake - ) - - if(MSVC) -@@ -45,7 +45,7 @@ if(MSVC) - ) - LIST(APPEND TOOLS_LINK_LIBS ${FFTW_LIBRARIES}) - else() -- LIST(APPEND TOOLS_LINK_LIBS m fftw3f) -+ LIST(APPEND TOOLS_LINK_LIBS m)# fftw3f) - endif() - - if(NOT libhackrf_SOURCE_DIR) diff --git a/tools/gpstest/patches/limeGPS/inc_ephem_array_size.patch b/tools/gpstest/patches/limeGPS/inc_ephem_array_size.patch deleted file mode 100644 index 9a3525d346..0000000000 --- a/tools/gpstest/patches/limeGPS/inc_ephem_array_size.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/gpssim.h b/gpssim.h -index c30b227..2ae0802 100644 ---- a/gpssim.h -+++ b/gpssim.h -@@ -75,7 +75,7 @@ - #define SC08 (8) - #define SC16 (16) - --#define EPHEM_ARRAY_SIZE (13) // for daily GPS broadcast ephemers file (brdc) -+#define EPHEM_ARRAY_SIZE (20) // for daily GPS broadcast ephemers file (brdc) - - /*! \brief Structure representing GPS time */ - typedef struct diff --git a/tools/gpstest/patches/limeGPS/makefile.patch b/tools/gpstest/patches/limeGPS/makefile.patch deleted file mode 100644 index f99ce551db..0000000000 --- a/tools/gpstest/patches/limeGPS/makefile.patch +++ /dev/null @@ -1,11 +0,0 @@ -diff --git a/makefile b/makefile -index 51bfabf..d0ea1eb 100644 ---- a/makefile -+++ b/makefile -@@ -1,5 +1,4 @@ - CC=gcc -O2 -Wall - - all: limegps.c gpssim.c -- $(CC) -o LimeGPS limegps.c gpssim.c -lm -lpthread -lLimeSuite -- -+ $(CC) -o LimeGPS limegps.c gpssim.c -lm -lpthread -lLimeSuite -I../LimeSuite/src -L../LimeSuite/builddir/src -Wl,-rpath="$(PWD)/../LimeSuite/builddir/src" diff --git a/tools/gpstest/patches/limeSuite/mcu_error.patch b/tools/gpstest/patches/limeSuite/mcu_error.patch deleted file mode 100644 index 91790a4a2b..0000000000 --- a/tools/gpstest/patches/limeSuite/mcu_error.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/src/lms7002m/LMS7002M_RxTxCalibrations.cpp b/src/lms7002m/LMS7002M_RxTxCalibrations.cpp -index 41a37044..ac29c6b6 100644 ---- a/src/lms7002m/LMS7002M_RxTxCalibrations.cpp -+++ b/src/lms7002m/LMS7002M_RxTxCalibrations.cpp -@@ -254,7 +254,7 @@ int LMS7002M::CalibrateTx(float_type bandwidth_Hz, bool useExtLoopback) - mcuControl->RunProcedure(useExtLoopback ? MCU_FUNCTION_CALIBRATE_TX_EXTLOOPB : MCU_FUNCTION_CALIBRATE_TX); - status = mcuControl->WaitForMCU(1000); - if(status != MCU_BD::MCU_NO_ERROR) -- return ReportError(EINVAL, "Tx Calibration: MCU error %i (%s)", status, MCU_BD::MCUStatusMessage(status)); -+ return -1; //ReportError(EINVAL, "Tx Calibration: MCU error %i (%s)", status, MCU_BD::MCUStatusMessage(status)); - } - - //sync registers to cache diff --git a/tools/gpstest/patches/limeSuite/reference_print.patch b/tools/gpstest/patches/limeSuite/reference_print.patch deleted file mode 100644 index 5bd7cdf1ed..0000000000 --- a/tools/gpstest/patches/limeSuite/reference_print.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/src/FPGA_common/FPGA_common.cpp b/src/FPGA_common/FPGA_common.cpp -index 4e81f33e..7381c475 100644 ---- a/src/FPGA_common/FPGA_common.cpp -+++ b/src/FPGA_common/FPGA_common.cpp -@@ -946,7 +946,7 @@ double FPGA::DetectRefClk(double fx3Clk) - - if (i == 0) - return -1; -- lime::info("Reference clock %1.2f MHz", clkTbl[i - 1] / 1e6); -+ //lime::info("Reference clock %1.2f MHz", clkTbl[i - 1] / 1e6); - return clkTbl[i - 1]; - } - diff --git a/tools/gpstest/rpc_server.py b/tools/gpstest/rpc_server.py deleted file mode 100644 index cdedd8ea57..0000000000 --- a/tools/gpstest/rpc_server.py +++ /dev/null @@ -1,185 +0,0 @@ -import os -import time -import shutil -from datetime import datetime -from collections import defaultdict - -import rpyc -from rpyc.utils.server import ThreadedServer - -#from openpilot.common.params import Params -import cereal.messaging as messaging -from openpilot.selfdrive.manager.process_config import managed_processes -from laika.lib.coordinates import ecef2geodetic - -DELTA = 0.001 -ALT_DELTA = 30 -MATCH_NUM = 10 -REPORT_STATS = 10 - -EPHEM_CACHE = "/data/params/d/LaikadEphemerisV3" -DOWNLOAD_CACHE = "/tmp/comma_download_cache" - -SERVER_LOG_FILE = "/tmp/fuzzy_server.log" -server_log = open(SERVER_LOG_FILE, "w+") - -def slog(msg): - server_log.write(f"{datetime.now().strftime('%H:%M:%S.%f')} | {msg}\n") - server_log.flush() - -def handle_laikad(msg): - if not hasattr(msg, 'correctedMeasurements'): - return None - - num_corr = len(msg.correctedMeasurements) - pos_ecef = msg.positionECEF.value - pos_geo = [] - if len(pos_ecef) > 0: - pos_geo = ecef2geodetic(pos_ecef) - - pos_std = msg.positionECEF.std - pos_valid = msg.positionECEF.valid - - slog(f"{num_corr} {pos_geo} {pos_ecef} {pos_std} {pos_valid}") - return pos_geo, (num_corr, pos_geo, list(pos_ecef), list(msg.positionECEF.std)) - -hw_msgs = 0 -ephem_msgs: dict = defaultdict(int) -def handle_ublox(msg): - global hw_msgs - - d = msg.to_dict() - - if 'hwStatus2' in d: - hw_msgs += 1 - - if 'ephemeris' in d: - ephem_msgs[msg.ephemeris.svId] += 1 - - num_meas = None - if 'measurementReport' in d: - num_meas = msg.measurementReport.numMeas - - return [hw_msgs, ephem_msgs, num_meas] - - -def start_procs(procs): - for p in procs: - managed_processes[p].start() - time.sleep(1) - -def kill_procs(procs, no_retry=False): - for p in procs: - managed_processes[p].stop() - time.sleep(1) - - if not no_retry: - for p in procs: - mp = managed_processes[p].proc - if mp is not None and mp.is_alive(): - managed_processes[p].stop() - time.sleep(3) - -def check_alive_procs(procs): - for p in procs: - mp = managed_processes[p].proc - if mp is None or not mp.is_alive(): - return False, p - return True, None - - -class RemoteCheckerService(rpyc.Service): - def on_connect(self, conn): - pass - - def on_disconnect(self, conn): - #kill_procs(self.procs, no_retry=False) - # this execution is delayed, it will kill the next run of laikad - # TODO: add polling to wait for everything is killed - pass - - def run_checker(self, slat, slon, salt, sockets, procs, timeout): - global hw_msgs, ephem_msgs - hw_msgs = 0 - ephem_msgs = defaultdict(int) - - slog(f"Run test: {slat} {slon} {salt}") - - # quectel_mod = Params().get_bool("UbloxAvailable") - - match_cnt = 0 - msg_cnt = 0 - stats_laikad = [] - stats_ublox = [] - - self.procs = procs - start_procs(procs) - sm = messaging.SubMaster(sockets) - - start_time = time.monotonic() - while True: - sm.update() - - if sm.updated['ubloxGnss']: - stats_ublox.append(handle_ublox(sm['ubloxGnss'])) - - if sm.updated['gnssMeasurements']: - pos_geo, stats = handle_laikad(sm['gnssMeasurements']) - if pos_geo is None or len(pos_geo) == 0: - continue - - match = all(abs(g-s) < DELTA for g,s in zip(pos_geo[:2], [slat, slon], strict=True)) - match &= abs(pos_geo[2] - salt) < ALT_DELTA - if match: - match_cnt += 1 - if match_cnt >= MATCH_NUM: - return True, "MATCH", f"After: {round(time.monotonic() - start_time, 4)}" - - # keep some stats for error reporting - stats_laikad.append(stats) - - if (msg_cnt % 10) == 0: - a, p = check_alive_procs(procs) - if not a: - return False, "PROC CRASH", f"{p}" - msg_cnt += 1 - - if (time.monotonic() - start_time) > timeout: - h = f"LAIKAD: {stats_laikad[-REPORT_STATS:]}" - if len(h) == 0: - h = f"UBLOX: {stats_ublox[-REPORT_STATS:]}" - return False, "TIMEOUT", h - - - def exposed_run_checker(self, slat, slon, salt, timeout=180, use_laikad=True): - try: - procs = [] - sockets = [] - - if use_laikad: - procs.append("laikad") # pigeond, ubloxd # might wanna keep them running - sockets += ['ubloxGnss', 'gnssMeasurements'] - - if os.path.exists(EPHEM_CACHE): - os.remove(EPHEM_CACHE) - shutil.rmtree(DOWNLOAD_CACHE, ignore_errors=True) - - ret = self.run_checker(slat, slon, salt, sockets, procs, timeout) - kill_procs(procs) - return ret - - except Exception as e: - # always make sure processes get killed - kill_procs(procs) - return False, "CHECKER CRASHED", f"{str(e)}" - - - def exposed_kill_procs(self): - kill_procs(self.procs, no_retry=True) - - -if __name__ == "__main__": - print(f"Sever Log written to: {SERVER_LOG_FILE}") - t = ThreadedServer(RemoteCheckerService, port=18861) - t.start() - diff --git a/tools/gpstest/run_unittest.sh b/tools/gpstest/run_unittest.sh deleted file mode 100755 index e0ca017a6d..0000000000 --- a/tools/gpstest/run_unittest.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# NOTE: can only run inside limeGPS test box! - -# run limeGPS with random static location -timeout 300 ./simulate_gps_signal.py 32.7518 -117.1962 & -gps_PID=$(ps -aux | grep -m 1 "timeout 300" | awk '{print $2}') - -echo "starting limeGPS..." -sleep 10 - -# run unit tests (skipped when module not present) -python -m unittest test_gps.py -python -m unittest test_gps_qcom.py - -kill $gps_PID diff --git a/tools/gpstest/setup.sh b/tools/gpstest/setup.sh deleted file mode 100755 index ddf41dd260..0000000000 --- a/tools/gpstest/setup.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -e - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd $DIR - -if [ ! -d LimeSuite ]; then - git clone https://github.com/myriadrf/LimeSuite.git - cd LimeSuite - # checkout latest version which has firmware updates available - git checkout v20.10.0 - git apply ../patches/limeSuite/* - mkdir builddir && cd builddir - cmake -DCMAKE_BUILD_TYPE=Release .. - make -j4 - cd ../.. -fi - -if [ ! -d LimeGPS ]; then - git clone https://github.com/osqzss/LimeGPS.git - cd LimeGPS - git apply ../patches/limeGPS/* - make - cd .. -fi diff --git a/tools/gpstest/setup_hackrf.sh b/tools/gpstest/setup_hackrf.sh deleted file mode 100755 index e504ec9447..0000000000 --- a/tools/gpstest/setup_hackrf.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -e - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd $DIR - -if [ ! -d gps-sdr-sim ]; then - git clone https://github.com/osqzss/gps-sdr-sim.git - cd gps-sdr-sim - make - cd .. -fi - -if [ ! -d hackrf ]; then - git clone https://github.com/greatscottgadgets/hackrf.git - cd hackrf/host - git apply ../../patches/hackrf.patch - cmake . - make -fi - diff --git a/tools/gpstest/simulate_gps_signal.py b/tools/gpstest/simulate_gps_signal.py deleted file mode 100755 index da0f64eaca..0000000000 --- a/tools/gpstest/simulate_gps_signal.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -import os -import random -import argparse -import datetime as dt -import subprocess as sp -from typing import Tuple - -from laika.downloader import download_nav -from laika.gps_time import GPSTime -from laika.helpers import ConstellationId - -cache_dir = '/tmp/gpstest/' - - -def download_rinex(): - # TODO: check if there is a better way to get the full brdc file for LimeGPS - gps_time = GPSTime.from_datetime(dt.datetime.utcnow()) - utc_time = dt.datetime.utcnow()# - dt.timedelta(1) - gps_time = GPSTime.from_datetime(dt.datetime(utc_time.year, utc_time.month, utc_time.day)) - return download_nav(gps_time, cache_dir, ConstellationId.GPS) - -def get_coords(lat, lon, s1, s2, o1=0, o2=0) -> Tuple[int, int]: - lat_add = random.random()*s1 + o1 - lon_add = random.random()*s2 + o2 - - lat = ((lat + lat_add + 90) % 180) - 90 - lon = ((lon + lon_add + 180) % 360) - 180 - return round(lat, 5), round(lon, 5) - -def get_continuous_coords(lat, lon) -> Tuple[int, int]: - # continuously move around the world - return get_coords(lat, lon, 0.01, 0.01) - -def get_random_coords(lat, lon) -> Tuple[int, int]: - # jump around the world - return get_coords(lat, lon, 20, 20, 10, 20) - -def run_limeSDR_loop(lat, lon, alt, contin_sim, rinex_file, timeout): - while True: - try: - # TODO: add starttime setting and altitude - # -t 2023/01/15,00:00:00 -T 2023/01/15,00:00:00 - # this needs to match the date of the navigation file - print(f"starting LimeGPS, Location: {lat} {lon} {alt}") - cmd = ["LimeGPS/LimeGPS", "-e", rinex_file, "-l", f"{lat},{lon},{alt}"] - print(f"CMD: {cmd}") - sp.check_output(cmd, stderr=sp.PIPE, timeout=timeout) - except KeyboardInterrupt: - print("stopping LimeGPS") - return - except sp.TimeoutExpired: - print("LimeGPS timeout reached!") - except Exception as e: - out_stderr = e.stderr.decode('utf-8')# pylint:disable=no-member - if "Device is busy." in out_stderr: - print("GPS simulation is already running, Device is busy!") - return - - print(f"LimeGPS crashed: {str(e)}") - print(f"stderr:\n{e.stderr.decode('utf-8')}")# pylint:disable=no-member - return - - if contin_sim: - lat, lon = get_continuous_coords(lat, lon) - else: - lat, lon = get_random_coords(lat, lon) - -def run_hackRF_loop(lat, lon, rinex_file, timeout): - - if timeout is not None: - print("no jump mode for hackrf!") - return - - try: - print(f"starting gps-sdr-sim, Location: {lat},{lon}") - # create 30second file and replay with hackrf endless - cmd = ["gps-sdr-sim/gps-sdr-sim", "-e", rinex_file, "-l", f"{lat},{lon},-200", "-d", "30"] - sp.check_output(cmd, stderr=sp.PIPE, timeout=timeout) - # created in current working directory - except Exception: - print("Failed to generate gpssim.bin") - - try: - print("starting hackrf_transfer") - # create 30second file and replay with hackrf endless - cmd = ["hackrf/host/hackrf-tools/src/hackrf_transfer", "-t", "gpssim.bin", - "-f", "1575420000", "-s", "2600000", "-a", "1", "-R"] - sp.check_output(cmd, stderr=sp.PIPE, timeout=timeout) - except KeyboardInterrupt: - print("stopping hackrf_transfer") - return - except Exception as e: - print(f"hackrf_transfer crashed:{str(e)}") - - -def main(lat, lon, alt, jump_sim, contin_sim, hackrf_mode): - - if hackrf_mode: - if not os.path.exists('hackrf'): - print("hackrf not found run 'setup_hackrf.sh' first") - return - - if not os.path.exists('gps-sdr-sim'): - print("gps-sdr-sim not found run 'setup_hackrf.sh' first") - return - - output = sp.check_output(["hackrf/host/hackrf-tools/src/hackrf_info"]) - if output.strip() == b"" or b"No HackRF boards found." in output: - print("No HackRF boards found!") - return - - else: - if not os.path.exists('LimeGPS'): - print("LimeGPS not found run 'setup.sh' first") - return - - if not os.path.exists('LimeSuite'): - print("LimeSuite not found run 'setup.sh' first") - return - - output = sp.check_output(["LimeSuite/builddir/LimeUtil/LimeUtil", "--find"]) - if output.strip() == b"": - print("No LimeSDR device found!") - return - print(f"Device: {output.strip().decode('utf-8')}") - - if lat == 0 and lon == 0: - lat, lon = get_random_coords(47.2020, 15.7403) - - rinex_file = download_rinex() - - timeout = None - if jump_sim: - timeout = 30 - - if hackrf_mode: - run_hackRF_loop(lat, lon, rinex_file, timeout) - else: - run_limeSDR_loop(lat, lon, alt, contin_sim, rinex_file, timeout) - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Simulate static [or random jumping] GPS signal.") - parser.add_argument("lat", type=float, nargs='?', default=0) - parser.add_argument("lon", type=float, nargs='?', default=0) - parser.add_argument("alt", type=float, nargs='?', default=0) - parser.add_argument("--jump", action="store_true", help="signal that jumps around the world") - parser.add_argument("--contin", action="store_true", help="continuously/slowly moving around the world") - parser.add_argument("--hackrf", action="store_true", help="hackrf mode (DEFAULT: LimeSDR)") - args = parser.parse_args() - main(args.lat, args.lon, args.alt, args.jump, args.contin, args.hackrf) diff --git a/tools/gpstest/test_gps.py b/tools/gpstest/test_gps.py deleted file mode 100644 index 98f1ad84b8..0000000000 --- a/tools/gpstest/test_gps.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -import time -import unittest -import struct - -from openpilot.common.params import Params -import cereal.messaging as messaging -import openpilot.system.sensord.pigeond as pd -from openpilot.system.hardware import TICI -from openpilot.selfdrive.test.helpers import with_processes - - -def read_events(service, duration_sec): - service_sock = messaging.sub_sock(service, timeout=0.1) - start_time_sec = time.monotonic() - events = [] - while time.monotonic() - start_time_sec < duration_sec: - events += messaging.drain_sock(service_sock) - time.sleep(0.1) - - assert len(events) != 0, f"No '{service}'events collected!" - return events - - -def create_backup(pigeon): - # controlled GNSS stop - pigeon.send(b"\xB5\x62\x06\x04\x04\x00\x00\x00\x08\x00\x16\x74") - - # store almanac in flash - pigeon.send(b"\xB5\x62\x09\x14\x04\x00\x00\x00\x00\x00\x21\xEC") - try: - if not pigeon.wait_for_ack(ack=pd.UBLOX_SOS_ACK, nack=pd.UBLOX_SOS_NACK): - raise RuntimeError("Could not store almanac") - except TimeoutError: - pass - - -def verify_ubloxgnss_data(socket: messaging.SubSocket, max_time: int): - start_time = 0 - end_time = 0 - events = messaging.drain_sock(socket) - assert len(events) != 0, "no ublxGnss measurements" - - for event in events: - if event.ubloxGnss.which() != "measurementReport": - continue - - if start_time == 0: - start_time = event.logMonoTime - - if event.ubloxGnss.measurementReport.numMeas != 0: - end_time = event.logMonoTime - break - - assert end_time != 0, "no ublox measurements received!" - - ttfm = (end_time - start_time)/1e9 - assert ttfm < max_time, f"Time to first measurement > {max_time}s, {ttfm}" - - # check for satellite count in measurements - sat_count = [] - end_id = events.index(event)# pylint:disable=undefined-loop-variable - for event in events[end_id:]: - if event.ubloxGnss.which() == "measurementReport": - sat_count.append(event.ubloxGnss.measurementReport.numMeas) - - num_sat = int(sum(sat_count)/len(sat_count)) - assert num_sat >= 5, f"Not enough satellites {num_sat} (TestBox setup!)" - - -def verify_gps_location(socket: messaging.SubSocket, max_time: int): - events = messaging.drain_sock(socket) - assert len(events) != 0, "no gpsLocationExternal measurements" - - start_time = events[0].logMonoTime - end_time = 0 - for event in events: - gps_valid = event.gpsLocationExternal.flags % 2 - - if gps_valid: - end_time = event.logMonoTime - break - - assert end_time != 0, "GPS location never converged!" - - ttfl = (end_time - start_time)/1e9 - assert ttfl < max_time, f"Time to first location > {max_time}s, {ttfl}" - - hacc = events[-1].gpsLocationExternal.accuracy - vacc = events[-1].gpsLocationExternal.verticalAccuracy - assert hacc < 20, f"Horizontal accuracy too high, {hacc}" - assert vacc < 45, f"Vertical accuracy too high, {vacc}" - - -def verify_time_to_first_fix(pigeon): - # get time to first fix from nav status message - nav_status = b"" - while True: - pigeon.send(b"\xb5\x62\x01\x03\x00\x00\x04\x0d") - nav_status = pigeon.receive() - if nav_status[:4] == b"\xb5\x62\x01\x03": - break - - values = struct.unpack(" 40s, {ttff}" - - -class TestGPS(unittest.TestCase): - @classmethod - def setUpClass(cls): - if not TICI: - raise unittest.SkipTest - - ublox_available = Params().get_bool("UbloxAvailable") - if not ublox_available: - raise unittest.SkipTest - - - def tearDown(self): - pd.set_power(False) - - @with_processes(['ubloxd']) - def test_a_ublox_reset(self): - - pigeon, pm = pd.create_pigeon() - pd.init_baudrate(pigeon) - assert pigeon.reset_device(), "Could not reset device!" - - pd.initialize_pigeon(pigeon) - - ugs = messaging.sub_sock("ubloxGnss", timeout=0.1) - gle = messaging.sub_sock("gpsLocationExternal", timeout=0.1) - - # receive some messages (restart after cold start takes up to 30seconds) - pd.run_receiving(pigeon, pm, 60) - - # store almanac for next test - create_backup(pigeon) - - verify_ubloxgnss_data(ugs, 60) - verify_gps_location(gle, 60) - - # skip for now, this might hang for a while - #verify_time_to_first_fix(pigeon) - - - @with_processes(['ubloxd']) - def test_b_ublox_almanac(self): - pigeon, pm = pd.create_pigeon() - pd.init_baudrate(pigeon) - - # device cold start - pigeon.send(b"\xb5\x62\x06\x04\x04\x00\xff\xff\x00\x00\x0c\x5d") - time.sleep(1) # wait for cold start - pd.init_baudrate(pigeon) - - # clear configuration - pigeon.send_with_ack(b"\xb5\x62\x06\x09\x0d\x00\x00\x00\x1f\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x17\x71\x5b") - - # restoring almanac backup - pigeon.send(b"\xB5\x62\x09\x14\x00\x00\x1D\x60") - status = pigeon.wait_for_backup_restore_status() - assert status == 2, "Could not restore almanac backup" - - pd.initialize_pigeon(pigeon) - - ugs = messaging.sub_sock("ubloxGnss", timeout=0.1) - gle = messaging.sub_sock("gpsLocationExternal", timeout=0.1) - - pd.run_receiving(pigeon, pm, 15) - verify_ubloxgnss_data(ugs, 15) - verify_gps_location(gle, 20) - - - @with_processes(['ubloxd']) - def test_c_ublox_startup(self): - pigeon, pm = pd.create_pigeon() - pd.init_baudrate(pigeon) - pd.initialize_pigeon(pigeon) - - ugs = messaging.sub_sock("ubloxGnss", timeout=0.1) - gle = messaging.sub_sock("gpsLocationExternal", timeout=0.1) - pd.run_receiving(pigeon, pm, 10) - verify_ubloxgnss_data(ugs, 10) - verify_gps_location(gle, 10) - - -if __name__ == "__main__": - unittest.main() diff --git a/tools/gpstest/test_gps_qcom.py b/tools/gpstest/test_gps_qcom.py deleted file mode 100644 index c399671715..0000000000 --- a/tools/gpstest/test_gps_qcom.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -import time -import unittest -import subprocess as sp - -from openpilot.common.params import Params -from openpilot.system.hardware import TICI -import cereal.messaging as messaging -from openpilot.selfdrive.manager.process_config import managed_processes - - -def exec_mmcli(cmd): - cmd = "mmcli -m 0 " + cmd - p = sp.Popen(cmd, shell=True, stdout=sp.PIPE, stderr=sp.PIPE) - return p.communicate() - - -def wait_for_location(socket, timeout): - while True: - events = messaging.drain_sock(socket) - for event in events: - if event.gpsLocation.flags % 2: - return False - - timeout -= 1 - if timeout <= 0: - return True - - time.sleep(0.1) - continue - - -class TestGPS(unittest.TestCase): - @classmethod - def setUpClass(cls): - if not TICI: - raise unittest.SkipTest - - ublox_available = Params().get_bool("UbloxAvailable") - if ublox_available: - raise unittest.SkipTest - - def test_a_quectel_cold_start(self): - # delete assistance data to enforce cold start for GNSS - # testing shows that this takes up to 20min - - _, err = exec_mmcli("--command='AT+QGPSDEL=0'") - assert len(err) == 0, f"GPSDEL failed: {err}" - - managed_processes['rawgpsd'].start() - start_time = time.monotonic() - glo = messaging.sub_sock("gpsLocation", timeout=0.1) - - timeout = 10*60*3 # 3 minute - timedout = wait_for_location(glo, timeout) - managed_processes['rawgpsd'].stop() - - assert timedout is False, "Waiting for location timed out (3min)!" - - duration = time.monotonic() - start_time - assert duration < 60, f"Received GPS location {duration}!" - - - def test_b_quectel_startup(self): - managed_processes['rawgpsd'].start() - start_time = time.monotonic() - glo = messaging.sub_sock("gpsLocation", timeout=0.1) - - timeout = 10*60 # 1 minute - timedout = wait_for_location(glo, timeout) - managed_processes['rawgpsd'].stop() - - assert timedout is False, "Waiting for location timed out (3min)!" - - duration = time.monotonic() - start_time - assert duration < 60, f"Received GPS location {duration}!" - - -if __name__ == "__main__": - unittest.main() diff --git a/tools/gpstest/test_laikad.py b/tools/gpstest/test_laikad.py deleted file mode 100644 index abb47d4bfc..0000000000 --- a/tools/gpstest/test_laikad.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import unittest - -import cereal.messaging as messaging -import openpilot.system.sensord.pigeond as pd - -from openpilot.common.params import Params -from openpilot.system.hardware import TICI -from openpilot.selfdrive.manager.process_config import managed_processes -from openpilot.selfdrive.test.helpers import with_processes - - -def wait_for_location(sm, timeout, con=10): - cons_meas = 0 - start_time = time.monotonic() - while (time.monotonic() - start_time) < timeout: - sm.update() - if not sm.updated["gnssMeasurements"]: - continue - - msg = sm["gnssMeasurements"] - cons_meas = (cons_meas + 1) if 'positionECEF' in msg.to_dict() else 0 - if cons_meas >= con: - return True - return False - - -class TestLaikad(unittest.TestCase): - @classmethod - def setUpClass(self): - if not TICI: - raise unittest.SkipTest - - ublox_available = Params().get_bool("UbloxAvailable") - if not ublox_available: - raise unittest.SkipTest - - def setUp(self): - # ensure laikad cold start - Params().remove("LaikadEphemerisV3") - os.environ["LAIKAD_NO_INTERNET"] = "1" - managed_processes['laikad'].start() - - def tearDown(self): - managed_processes['laikad'].stop() - - - @with_processes(['pigeond', 'ubloxd']) - def test_laikad_cold_start(self): - time.sleep(5) - - start_time = time.monotonic() - sm = messaging.SubMaster(["gnssMeasurements"]) - - success = wait_for_location(sm, 60*2, con=10) - duration = time.monotonic() - start_time - - assert success, "Waiting for location timed out (2min)!" - assert duration < 60, f"Received Location {duration}!" - - - @with_processes(['ubloxd']) - def test_laikad_ublox_reset_start(self): - time.sleep(2) - - pigeon, pm = pd.create_pigeon() - pd.init_baudrate(pigeon) - assert pigeon.reset_device(), "Could not reset device!" - - laikad_sock = messaging.sub_sock("gnssMeasurements", timeout=0.1) - ublox_gnss_sock = messaging.sub_sock("ubloxGnss", timeout=0.1) - - pd.init_baudrate(pigeon) - pd.initialize_pigeon(pigeon) - pd.run_receiving(pigeon, pm, 180) - - ublox_msgs = messaging.drain_sock(ublox_gnss_sock) - laikad_msgs = messaging.drain_sock(laikad_sock) - - gps_ephem_cnt = 0 - glonass_ephem_cnt = 0 - for um in ublox_msgs: - if um.ubloxGnss.which() == 'ephemeris': - gps_ephem_cnt += 1 - elif um.ubloxGnss.which() == 'glonassEphemeris': - glonass_ephem_cnt += 1 - - assert gps_ephem_cnt > 0, "NO gps ephemeris collected!" - assert glonass_ephem_cnt > 0, "NO glonass ephemeris collected!" - - pos_meas = 0 - duration = -1 - for lm in laikad_msgs: - pos_meas = (pos_meas + 1) if 'positionECEF' in lm.gnssMeasurements.to_dict() else 0 - if pos_meas > 5: - duration = (lm.logMonoTime - laikad_msgs[0].logMonoTime)*1e-9 - break - - assert pos_meas > 5, "NOT enough positions at end of read!" - assert duration < 120, "Laikad took too long to get a Position!" - -if __name__ == "__main__": - unittest.main() diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index 07bb8ac9a4..6753afffb9 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -54,8 +54,8 @@ fi eval "$(pyenv init --path)" echo "update pip" -pip install pip==23.2.1 -pip install poetry==1.5.1 +pip install pip==23.3 +pip install poetry==1.6.1 poetry config virtualenvs.prefer-active-python true --local poetry config virtualenvs.in-project true --local diff --git a/tools/joystick/README.md b/tools/joystick/README.md index c74e51146c..65422afe80 100644 --- a/tools/joystick/README.md +++ b/tools/joystick/README.md @@ -61,4 +61,4 @@ Now start your car and openpilot should go into joystick mode with an alert on s Make sure the conditions are met in the panda to allow controls (e.g. cruise control engaged). You can also make a modification to the panda code to always allow controls. -![](steer.gif) +![](https://github.com/commaai/openpilot/assets/8762862/e640cbca-cb7a-4dcb-abce-b23b036ad8e7) diff --git a/tools/joystick/steer.gif b/tools/joystick/steer.gif deleted file mode 100644 index 8331b50de9..0000000000 Binary files a/tools/joystick/steer.gif and /dev/null differ diff --git a/tools/lib/auth_config.py b/tools/lib/auth_config.py index 8863dd57a2..e0989f02ea 100644 --- a/tools/lib/auth_config.py +++ b/tools/lib/auth_config.py @@ -1,24 +1,16 @@ import json import os from openpilot.common.file_helpers import mkdirs_exists_ok -from openpilot.system.hardware import PC +from openpilot.system.hardware.hw import Paths class MissingAuthConfigError(Exception): pass -if PC: - CONFIG_DIR = os.path.expanduser('~/.comma') -else: - CONFIG_DIR = "/tmp/.comma" - -mkdirs_exists_ok(CONFIG_DIR) - - def get_token(): try: - with open(os.path.join(CONFIG_DIR, 'auth.json')) as f: + with open(os.path.join(Paths.config_root(), 'auth.json')) as f: auth = json.load(f) return auth['access_token'] except Exception: @@ -26,9 +18,13 @@ def get_token(): def set_token(token): - with open(os.path.join(CONFIG_DIR, 'auth.json'), 'w') as f: + mkdirs_exists_ok(Paths.config_root()) + with open(os.path.join(Paths.config_root(), 'auth.json'), 'w') as f: json.dump({'access_token': token}, f) def clear_token(): - os.unlink(os.path.join(CONFIG_DIR, 'auth.json')) + try: + os.unlink(os.path.join(Paths.config_root(), 'auth.json')) + except FileNotFoundError: + pass diff --git a/tools/lib/cache.py b/tools/lib/cache.py index d142955e59..fd214f6bb5 100644 --- a/tools/lib/cache.py +++ b/tools/lib/cache.py @@ -2,10 +2,10 @@ import os import urllib.parse from openpilot.common.file_helpers import mkdirs_exists_ok -DEFAULT_CACHE_DIR = os.path.expanduser("~/.commacache") +DEFAULT_CACHE_DIR = os.getenv("CACHE_ROOT", os.path.expanduser("~/.commacache")) -def cache_path_for_file_path(fn, cache_prefix=None): - dir_ = os.path.join(DEFAULT_CACHE_DIR, "local") +def cache_path_for_file_path(fn, cache_dir=DEFAULT_CACHE_DIR): + dir_ = os.path.join(cache_dir, "local") mkdirs_exists_ok(dir_) fn_parsed = urllib.parse.urlparse(fn) if fn_parsed.scheme == '': diff --git a/tools/lib/filereader.py b/tools/lib/filereader.py index 5ac23d57ec..4aec965f1a 100644 --- a/tools/lib/filereader.py +++ b/tools/lib/filereader.py @@ -3,9 +3,13 @@ from openpilot.tools.lib.url_file import URLFile DATA_ENDPOINT = os.getenv("DATA_ENDPOINT", "http://data-raw.comma.internal/") -def FileReader(fn, debug=False): +def resolve_name(fn): if fn.startswith("cd:/"): - fn = fn.replace("cd:/", DATA_ENDPOINT) + return fn.replace("cd:/", DATA_ENDPOINT) + return fn + +def FileReader(fn, debug=False): + fn = resolve_name(fn) if fn.startswith(("http://", "https://")): return URLFile(fn, debug=debug) return open(fn, "rb") diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index cbc9310790..275b9b65b8 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -3,7 +3,6 @@ import os import pickle import struct import subprocess -import tempfile import threading from enum import IntEnum from functools import wraps @@ -12,11 +11,12 @@ import numpy as np from lru import LRU import _io -from openpilot.tools.lib.cache import cache_path_for_file_path +from openpilot.tools.lib.cache import cache_path_for_file_path, DEFAULT_CACHE_DIR from openpilot.tools.lib.exceptions import DataUnreadableError +from openpilot.tools.lib.vidindex import hevc_index from openpilot.common.file_helpers import atomic_write_in_dir -from openpilot.tools.lib.filereader import FileReader +from openpilot.tools.lib.filereader import FileReader, resolve_name HEVC_SLICE_B = 0 HEVC_SLICE_P = 1 @@ -59,62 +59,35 @@ def fingerprint_video(fn): def ffprobe(fn, fmt=None): - cmd = ["ffprobe", - "-v", "quiet", - "-print_format", "json", - "-show_format", "-show_streams"] + fn = resolve_name(fn) + cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams"] if fmt: cmd += ["-f", fmt] - cmd += [fn] + cmd += ["-i", "-"] try: - ffprobe_output = subprocess.check_output(cmd) + with FileReader(fn) as f: + ffprobe_output = subprocess.check_output(cmd, input=f.read(4096)) except subprocess.CalledProcessError as e: raise DataUnreadableError(fn) from e return json.loads(ffprobe_output) -def vidindex(fn, typ): - vidindex_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vidindex") - vidindex = os.path.join(vidindex_dir, "vidindex") - - subprocess.check_call(["make"], cwd=vidindex_dir, stdout=subprocess.DEVNULL) - - with tempfile.NamedTemporaryFile() as prefix_f, \ - tempfile.NamedTemporaryFile() as index_f: - try: - subprocess.check_call([vidindex, typ, fn, prefix_f.name, index_f.name]) - except subprocess.CalledProcessError as e: - raise DataUnreadableError(f"vidindex failed on file {fn}") from e - with open(index_f.name, "rb") as f: - index = f.read() - with open(prefix_f.name, "rb") as f: - prefix = f.read() - - index = np.frombuffer(index, np.uint32).reshape(-1, 2) - - assert index[-1, 0] == 0xFFFFFFFF - assert index[-1, 1] == os.path.getsize(fn) - - return index, prefix - - def cache_fn(func): @wraps(func) def cache_inner(fn, *args, **kwargs): if kwargs.pop('no_cache', None): cache_path = None else: - cache_prefix = kwargs.pop('cache_prefix', None) - cache_path = cache_path_for_file_path(fn, cache_prefix) + cache_dir = kwargs.pop('cache_dir', DEFAULT_CACHE_DIR) + cache_path = cache_path_for_file_path(fn, cache_dir) if cache_path and os.path.exists(cache_path): with open(cache_path, "rb") as cache_file: cache_value = pickle.load(cache_file) else: cache_value = func(fn, *args, **kwargs) - if cache_path: with atomic_write_in_dir(cache_path, mode="wb", overwrite=True) as cache_file: pickle.dump(cache_value, cache_file, -1) @@ -125,13 +98,13 @@ def cache_fn(func): @cache_fn -def index_stream(fn, typ): - assert typ in ("hevc", ) +def index_stream(fn, ft): + if ft != FrameType.h265_stream: + raise NotImplementedError("Only h265 supported") - with FileReader(fn) as f: - assert os.path.exists(f.name), fn - index, prefix = vidindex(f.name, typ) - probe = ffprobe(f.name, typ) + frame_types, dat_len, prefix = hevc_index(fn) + index = np.array(frame_types + [(0xFFFFFFFF, dat_len)], dtype=np.uint32) + probe = ffprobe(fn, "hevc") return { 'index': index, @@ -140,42 +113,8 @@ def index_stream(fn, typ): } -def index_videos(camera_paths, cache_prefix=None): - """Requires that paths in camera_paths are contiguous and of the same type.""" - if len(camera_paths) < 1: - raise ValueError("must provide at least one video to index") - - frame_type = fingerprint_video(camera_paths[0]) - for fn in camera_paths: - index_video(fn, frame_type, cache_prefix) - - -def index_video(fn, frame_type=None, cache_prefix=None): - cache_path = cache_path_for_file_path(fn, cache_prefix) - - if os.path.exists(cache_path): - return - - if frame_type is None: - frame_type = fingerprint_video(fn[0]) - - if frame_type == FrameType.h265_stream: - index_stream(fn, "hevc", cache_prefix=cache_prefix) - else: - raise NotImplementedError("Only h265 supported") - - -def get_video_index(fn, frame_type, cache_prefix=None): - cache_path = cache_path_for_file_path(fn, cache_prefix) - - if not os.path.exists(cache_path): - index_video(fn, frame_type, cache_prefix) - - if not os.path.exists(cache_path): - return None - with open(cache_path, "rb") as cache_file: - return pickle.load(cache_file) - +def get_video_index(fn, frame_type, cache_dir=DEFAULT_CACHE_DIR): + return index_stream(fn, frame_type, cache_dir=cache_dir) def read_file_check_size(f, sz, cookie): buff = bytearray(sz) @@ -228,31 +167,21 @@ def rgb24tonv12(rgb): def decompress_video_data(rawdat, vid_fmt, w, h, pix_fmt): - # using a tempfile is much faster than proc.communicate for some reason - - with tempfile.TemporaryFile() as tmpf: - tmpf.write(rawdat) - tmpf.seek(0) - - threads = os.getenv("FFMPEG_THREADS", "0") - cuda = os.getenv("FFMPEG_CUDA", "0") == "1" - args = ["ffmpeg", - "-threads", threads, - "-hwaccel", "none" if not cuda else "cuda", - "-c:v", "hevc", - "-vsync", "0", - "-f", vid_fmt, - "-flags2", "showall", - "-i", "pipe:0", - "-threads", threads, - "-f", "rawvideo", - "-pix_fmt", pix_fmt, - "pipe:1"] - with subprocess.Popen(args, stdin=tmpf, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) as proc: - # dat = proc.communicate()[0] - dat = proc.stdout.read() - if proc.wait() != 0: - raise DataUnreadableError("ffmpeg failed") + threads = os.getenv("FFMPEG_THREADS", "0") + cuda = os.getenv("FFMPEG_CUDA", "0") == "1" + args = ["ffmpeg", "-v", "quiet", + "-threads", threads, + "-hwaccel", "none" if not cuda else "cuda", + "-c:v", "hevc", + "-vsync", "0", + "-f", vid_fmt, + "-flags2", "showall", + "-i", "-", + "-threads", threads, + "-f", "rawvideo", + "-pix_fmt", pix_fmt, + "-"] + dat = subprocess.check_output(args, input=rawdat) if pix_fmt == "rgb24": ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) @@ -284,13 +213,13 @@ class BaseFrameReader: raise NotImplementedError -def FrameReader(fn, cache_prefix=None, readahead=False, readbehind=False, index_data=None): +def FrameReader(fn, cache_dir=DEFAULT_CACHE_DIR, readahead=False, readbehind=False, index_data=None): frame_type = fingerprint_video(fn) if frame_type == FrameType.raw: return RawFrameReader(fn) elif frame_type in (FrameType.h265_stream,): if not index_data: - index_data = get_video_index(fn, frame_type, cache_prefix) + index_data = get_video_index(fn, frame_type, cache_dir) return StreamFrameReader(fn, frame_type, index_data, readahead=readahead, readbehind=readbehind) else: raise NotImplementedError(frame_type) diff --git a/tools/lib/kbhit.py b/tools/lib/kbhit.py old mode 100644 new mode 100755 diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py index e528996f32..4af922c774 100755 --- a/tools/lib/logreader.py +++ b/tools/lib/logreader.py @@ -6,11 +6,14 @@ import urllib.parse import capnp import warnings +from typing import Iterable, Iterator from cereal import log as capnp_log from openpilot.tools.lib.filereader import FileReader from openpilot.tools.lib.route import Route, SegmentName +LogIterable = Iterable[capnp._DynamicStructReader] + # this is an iterator itself, and uses private variables from LogReader class MultiLogIterator: def __init__(self, log_paths, sort_by_time=False): @@ -30,7 +33,7 @@ class MultiLogIterator: return self._log_readers[i] - def __iter__(self): + def __iter__(self) -> Iterator[capnp._DynamicStructReader]: return self def _inc(self): @@ -107,7 +110,7 @@ class LogReader: def from_bytes(cls, dat): return cls("", dat=dat) - def __iter__(self): + def __iter__(self) -> Iterator[capnp._DynamicStructReader]: for ent in self._ents: if self._only_union_types: try: diff --git a/tools/lib/tests/test_caching.py b/tools/lib/tests/test_caching.py old mode 100644 new mode 100755 index bd93cd93ec..73ed843869 --- a/tools/lib/tests/test_caching.py +++ b/tools/lib/tests/test_caching.py @@ -1,8 +1,13 @@ #!/usr/bin/env python3 import os import unittest + +from pathlib import Path +from parameterized import parameterized +from unittest import mock + +from openpilot.system.hardware.hw import Paths from openpilot.tools.lib.url_file import URLFile -from openpilot.selfdrive.test.helpers import temporary_cache_dir class TestFileDownload(unittest.TestCase): @@ -31,8 +36,7 @@ class TestFileDownload(unittest.TestCase): self.assertEqual(file_cached.get_length(), file_downloaded.get_length()) self.assertEqual(response_cached, response_downloaded) - @temporary_cache_dir - def test_small_file(self, temp_dir): + def test_small_file(self): # Make sure we don't force cache os.environ["FILEREADER_CACHE"] = "0" small_file_url = "https://raw.githubusercontent.com/commaai/openpilot/master/docs/SAFETY.md" @@ -52,8 +56,7 @@ class TestFileDownload(unittest.TestCase): for i in range(length // 100): self.compare_loads(small_file_url, 100 * i, 100) - @temporary_cache_dir - def test_large_file(self, temp_dir): + def test_large_file(self): large_file_url = "https://commadataci.blob.core.windows.net/openpilotci/0375fdf7b1ce594d/2019-06-13--08-32-25/3/qlog.bz2" # Load the end 100 bytes of both files file_large = URLFile(large_file_url) @@ -62,6 +65,34 @@ class TestFileDownload(unittest.TestCase): self.compare_loads(large_file_url, length - 100, 100) self.compare_loads(large_file_url) + @parameterized.expand([(True, ), (False, )]) + def test_recover_from_missing_file(self, cache_enabled): + os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" + + file_url = "http://localhost:5001/test.png" + + file_exists = False + + def get_length_online_mock(self): + if file_exists: + return 4 + return -1 + + patch_length = mock.patch.object(URLFile, "get_length_online", get_length_online_mock) + patch_length.start() + try: + length = URLFile(file_url).get_length() + self.assertEqual(length, -1) + + file_exists = True + length = URLFile(file_url).get_length() + self.assertEqual(length, 4) + finally: + tempfile_length = Path(Paths.download_cache_root()) / "ba2119904385654cb0105a2da174875f8e7648db175f202ecae6d6428b0e838f_length" + if tempfile_length.exists(): + tempfile_length.unlink() + patch_length.stop() + if __name__ == "__main__": unittest.main() diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index e93f8e715c..315ade514b 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -1,25 +1,26 @@ import os import time -import tempfile import threading -import urllib.parse import pycurl from hashlib import sha256 from io import BytesIO from tenacity import retry, wait_random_exponential, stop_after_attempt from openpilot.common.file_helpers import mkdirs_exists_ok, atomic_write_in_dir +from openpilot.system.hardware.hw import Paths # Cache chunk size K = 1000 CHUNK_SIZE = 1000 * K -CACHE_DIR = os.environ.get("COMMA_CACHE", "/tmp/comma_download_cache/") - def hash_256(link): hsh = str(sha256((link.split("?")[0]).encode('utf-8')).hexdigest()) return hsh +class URLFileException(Exception): + pass + + class URLFile: _tlocal = threading.local() @@ -38,7 +39,8 @@ class URLFile: self._curl = self._tlocal.curl except AttributeError: self._curl = self._tlocal.curl = pycurl.Curl() - mkdirs_exists_ok(CACHE_DIR) + if not self._force_download: + mkdirs_exists_ok(Paths.download_cache_root()) def __enter__(self): return self @@ -66,15 +68,16 @@ class URLFile: def get_length(self): if self._length is not None: return self._length - file_length_path = os.path.join(CACHE_DIR, hash_256(self._url) + "_length") - if os.path.exists(file_length_path) and not self._force_download: + + file_length_path = os.path.join(Paths.download_cache_root(), hash_256(self._url) + "_length") + if not self._force_download and os.path.exists(file_length_path): with open(file_length_path) as file_length: - content = file_length.read() - self._length = int(content) - return self._length + content = file_length.read() + self._length = int(content) + return self._length self._length = self.get_length_online() - if not self._force_download: + if not self._force_download and self._length != -1: with atomic_write_in_dir(file_length_path, mode="w") as file_length: file_length.write(str(self._length)) return self._length @@ -93,7 +96,7 @@ class URLFile: self._pos = position chunk_number = self._pos / CHUNK_SIZE file_name = hash_256(self._url) + "_" + str(chunk_number) - full_path = os.path.join(CACHE_DIR, str(file_name)) + full_path = os.path.join(Paths.download_cache_root(), str(file_name)) data = None # If we don't have a file, download it if not os.path.exists(full_path): @@ -159,11 +162,11 @@ class URLFile: response_code = c.getinfo(pycurl.RESPONSE_CODE) if response_code == 416: # Requested Range Not Satisfiable - raise Exception(f"Error, range out of bounds {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") + raise URLFileException(f"Error, range out of bounds {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") if download_range and response_code != 206: # Partial Content - raise Exception(f"Error, requested range but got unexpected response {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") + raise URLFileException(f"Error, requested range but got unexpected response {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") if (not download_range) and response_code != 200: # OK - raise Exception(f"Error {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") + raise URLFileException(f"Error {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") ret = dats.getvalue() self._pos += len(ret) @@ -174,24 +177,4 @@ class URLFile: @property def name(self): - """Returns a local path to file with the URLFile's contents. - - This can be used to interface with modules that require local files. - """ - if self._local_file is None: - _, ext = os.path.splitext(urllib.parse.urlparse(self._url).path) - local_fd, local_path = tempfile.mkstemp(suffix=ext) - try: - os.write(local_fd, self.read()) - local_file = open(local_path, "rb") - except Exception: - os.remove(local_path) - raise - finally: - os.close(local_fd) - - self._local_file = local_file - self.read = self._local_file.read - self.seek = self._local_file.seek - - return self._local_file.name + return self._url diff --git a/tools/lib/vidindex.py b/tools/lib/vidindex.py new file mode 100755 index 0000000000..8156faba6b --- /dev/null +++ b/tools/lib/vidindex.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +import argparse +import os +import struct +from enum import IntEnum +from typing import Tuple + +from openpilot.tools.lib.filereader import FileReader + +DEBUG = int(os.getenv("DEBUG", "0")) + +# compare to ffmpeg parsing +# ffmpeg -i -c copy -bsf:v trace_headers -f null - 2>&1 | grep -B4 -A32 '] 0 ' + +# H.265 specification +# https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.265-201802-S!!PDF-E&type=items + +NAL_UNIT_START_CODE = b"\x00\x00\x01" +NAL_UNIT_START_CODE_SIZE = len(NAL_UNIT_START_CODE) +NAL_UNIT_HEADER_SIZE = 2 + +class HevcNalUnitType(IntEnum): + TRAIL_N = 0 # RBSP structure: slice_segment_layer_rbsp( ) + TRAIL_R = 1 # RBSP structure: slice_segment_layer_rbsp( ) + TSA_N = 2 # RBSP structure: slice_segment_layer_rbsp( ) + TSA_R = 3 # RBSP structure: slice_segment_layer_rbsp( ) + STSA_N = 4 # RBSP structure: slice_segment_layer_rbsp( ) + STSA_R = 5 # RBSP structure: slice_segment_layer_rbsp( ) + RADL_N = 6 # RBSP structure: slice_segment_layer_rbsp( ) + RADL_R = 7 # RBSP structure: slice_segment_layer_rbsp( ) + RASL_N = 8 # RBSP structure: slice_segment_layer_rbsp( ) + RASL_R = 9 # RBSP structure: slice_segment_layer_rbsp( ) + RSV_VCL_N10 = 10 + RSV_VCL_R11 = 11 + RSV_VCL_N12 = 12 + RSV_VCL_R13 = 13 + RSV_VCL_N14 = 14 + RSV_VCL_R15 = 15 + BLA_W_LP = 16 # RBSP structure: slice_segment_layer_rbsp( ) + BLA_W_RADL = 17 # RBSP structure: slice_segment_layer_rbsp( ) + BLA_N_LP = 18 # RBSP structure: slice_segment_layer_rbsp( ) + IDR_W_RADL = 19 # RBSP structure: slice_segment_layer_rbsp( ) + IDR_N_LP = 20 # RBSP structure: slice_segment_layer_rbsp( ) + CRA_NUT = 21 # RBSP structure: slice_segment_layer_rbsp( ) + RSV_IRAP_VCL22 = 22 + RSV_IRAP_VCL23 = 23 + RSV_VCL24 = 24 + RSV_VCL25 = 25 + RSV_VCL26 = 26 + RSV_VCL27 = 27 + RSV_VCL28 = 28 + RSV_VCL29 = 29 + RSV_VCL30 = 30 + RSV_VCL31 = 31 + VPS_NUT = 32 # RBSP structure: video_parameter_set_rbsp( ) + SPS_NUT = 33 # RBSP structure: seq_parameter_set_rbsp( ) + PPS_NUT = 34 # RBSP structure: pic_parameter_set_rbsp( ) + AUD_NUT = 35 + EOS_NUT = 36 + EOB_NUT = 37 + FD_NUT = 38 + PREFIX_SEI_NUT = 39 + SUFFIX_SEI_NUT = 40 + RSV_NVCL41 = 41 + RSV_NVCL42 = 42 + RSV_NVCL43 = 43 + RSV_NVCL44 = 44 + RSV_NVCL45 = 45 + RSV_NVCL46 = 46 + RSV_NVCL47 = 47 + UNSPEC48 = 48 + UNSPEC49 = 49 + UNSPEC50 = 50 + UNSPEC51 = 51 + UNSPEC52 = 52 + UNSPEC53 = 53 + UNSPEC54 = 54 + UNSPEC55 = 55 + UNSPEC56 = 56 + UNSPEC57 = 57 + UNSPEC58 = 58 + UNSPEC59 = 59 + UNSPEC60 = 60 + UNSPEC61 = 61 + UNSPEC62 = 62 + UNSPEC63 = 63 + +# B.2.2 Byte stream NAL unit semantics +# - The nal_unit_type within the nal_unit( ) syntax structure is equal to VPS_NUT, SPS_NUT or PPS_NUT. +# - The byte stream NAL unit syntax structure contains the first NAL unit of an access unit in decoding +# order, as specified in clause 7.4.2.4.4. +HEVC_PARAMETER_SET_NAL_UNITS = ( + HevcNalUnitType.VPS_NUT, + HevcNalUnitType.SPS_NUT, + HevcNalUnitType.PPS_NUT, +) + +# 3.29 coded slice segment NAL unit: A NAL unit that has nal_unit_type in the range of TRAIL_N to RASL_R, +# inclusive, or in the range of BLA_W_LP to RSV_IRAP_VCL23, inclusive, which indicates that the NAL unit +# contains a coded slice segment +HEVC_CODED_SLICE_SEGMENT_NAL_UNITS = ( + HevcNalUnitType.TRAIL_N, + HevcNalUnitType.TRAIL_R, + HevcNalUnitType.TSA_N, + HevcNalUnitType.TSA_R, + HevcNalUnitType.STSA_N, + HevcNalUnitType.STSA_R, + HevcNalUnitType.RADL_N, + HevcNalUnitType.RADL_R, + HevcNalUnitType.RASL_N, + HevcNalUnitType.RASL_R, + HevcNalUnitType.BLA_W_LP, + HevcNalUnitType.BLA_W_RADL, + HevcNalUnitType.BLA_N_LP, + HevcNalUnitType.IDR_W_RADL, + HevcNalUnitType.IDR_N_LP, + HevcNalUnitType.CRA_NUT, +) + +class VideoFileInvalid(Exception): + pass + +def get_ue(dat: bytes, start_idx: int, skip_bits: int) -> Tuple[int, int]: + prefix_val = 0 + prefix_len = 0 + suffix_val = 0 + suffix_len = 0 + + i = start_idx + while i < len(dat): + j = 7 + while j >= 0: + if skip_bits > 0: + skip_bits -= 1 + elif prefix_val == 0: + prefix_val = (dat[i] >> j) & 1 + prefix_len += 1 + else: + suffix_val = (suffix_val << 1) | ((dat[i] >> j) & 1) + suffix_len += 1 + j -= 1 + + if prefix_val == 1 and prefix_len - 1 == suffix_len: + val = 2**(prefix_len-1) - 1 + suffix_val + size = prefix_len + suffix_len + return val, size + i += 1 + + raise VideoFileInvalid("invalid exponential-golomb code") + +def require_nal_unit_start(dat: bytes, nal_unit_start: int) -> None: + if nal_unit_start < 1: + raise ValueError("start index must be greater than zero") + + if dat[nal_unit_start:nal_unit_start + NAL_UNIT_START_CODE_SIZE] != NAL_UNIT_START_CODE: + raise VideoFileInvalid("data must begin with start code") + +def get_hevc_nal_unit_length(dat: bytes, nal_unit_start: int) -> int: + try: + pos = dat.index(NAL_UNIT_START_CODE, nal_unit_start + NAL_UNIT_START_CODE_SIZE) + except ValueError: + pos = -1 + + # length of NAL unit is byte count up to next NAL unit start index + nal_unit_len = (pos if pos != -1 else len(dat)) - nal_unit_start + if DEBUG: + print(" nal_unit_len:", nal_unit_len) + return nal_unit_len + +def get_hevc_nal_unit_type(dat: bytes, nal_unit_start: int) -> HevcNalUnitType: + # 7.3.1.2 NAL unit header syntax + # nal_unit_header( ) { // descriptor + # forbidden_zero_bit f(1) + # nal_unit_type u(6) + # nuh_layer_id u(6) + # nuh_temporal_id_plus1 u(3) + # } + header_start = nal_unit_start + NAL_UNIT_START_CODE_SIZE + nal_unit_header = dat[header_start:header_start + NAL_UNIT_HEADER_SIZE] + if len(nal_unit_header) != 2: + raise VideoFileInvalid("data to short to contain nal unit header") + nal_unit_type = HevcNalUnitType((nal_unit_header[0] >> 1) & 0x3F) + if DEBUG: + print(" nal_unit_type:", nal_unit_type.name, f"({nal_unit_type.value})") + return nal_unit_type + +def get_hevc_slice_type(dat: bytes, nal_unit_start: int, nal_unit_type: HevcNalUnitType) -> Tuple[int, bool]: + # 7.3.2.9 Slice segment layer RBSP syntax + # slice_segment_layer_rbsp( ) { + # slice_segment_header( ) + # slice_segment_data( ) + # rbsp_slice_segment_trailing_bits( ) + # } + # ... + # 7.3.6.1 General slice segment header syntax + # slice_segment_header( ) { // descriptor + # first_slice_segment_in_pic_flag u(1) + # if( nal_unit_type >= BLA_W_LP && nal_unit_type <= RSV_IRAP_VCL23 ) + # no_output_of_prior_pics_flag u(1) + # slice_pic_parameter_set_id ue(v) + # if( !first_slice_segment_in_pic_flag ) { + # if( dependent_slice_segments_enabled_flag ) + # dependent_slice_segment_flag u(1) + # slice_segment_address u(v) + # } + # if( !dependent_slice_segment_flag ) { + # for( i = 0; i < num_extra_slice_header_bits; i++ ) + # slice_reserved_flag[ i ] u(1) + # slice_type ue(v) + # ... + + rbsp_start = nal_unit_start + NAL_UNIT_START_CODE_SIZE + NAL_UNIT_HEADER_SIZE + skip_bits = 0 + + # 7.4.7.1 General slice segment header semantics + # first_slice_segment_in_pic_flag equal to 1 specifies that the slice segment is the first slice segment of the picture in + # decoding order. first_slice_segment_in_pic_flag equal to 0 specifies that the slice segment is not the first slice segment + # of the picture in decoding order. + is_first_slice = dat[rbsp_start] >> 7 & 1 == 1 + if not is_first_slice: + # TODO: parse dependent_slice_segment_flag and slice_segment_address and get real slice_type + # for now since we don't use it return -1 for slice_type + return (-1, is_first_slice) + skip_bits += 1 # skip past first_slice_segment_in_pic_flag + + if nal_unit_type >= HevcNalUnitType.BLA_W_LP and nal_unit_type <= HevcNalUnitType.RSV_IRAP_VCL23: + # 7.4.7.1 General slice segment header semantics + # no_output_of_prior_pics_flag affects the output of previously-decoded pictures in the decoded picture buffer after the + # decoding of an IDR or a BLA picture that is not the first picture in the bitstream as specified in Annex C. + skip_bits += 1 # skip past no_output_of_prior_pics_flag + + # 7.4.7.1 General slice segment header semantics + # slice_pic_parameter_set_id specifies the value of pps_pic_parameter_set_id for the PPS in use. + # The value of slice_pic_parameter_set_id shall be in the range of 0 to 63, inclusive. + _, size = get_ue(dat, rbsp_start, skip_bits) + skip_bits += size # skip past slice_pic_parameter_set_id + + # 7.4.3.3.1 General picture parameter set RBSP semanal_unit_lenntics + # num_extra_slice_header_bits specifies the number of extra slice header bits that are present in the slice header RBSP + # for coded pictures referring to the PPS. The value of num_extra_slice_header_bits shall be in the range of 0 to 2, inclusive, + # in bitstreams conforming to this version of this Specification. Other values for num_extra_slice_header_bits are reserved + # for future use by ITU-T | ISO/IEC. However, decoders shall allow num_extra_slice_header_bits to have any value. + # TODO: get from PPS_NUT pic_parameter_set_rbsp( ) for corresponding slice_pic_parameter_set_id + num_extra_slice_header_bits = 0 + skip_bits += num_extra_slice_header_bits + + # 7.4.7.1 General slice segment header semantics + # slice_type specifies the coding type of the slice according to Table 7-7. + # Table 7-7 - Name association to slice_type + # slice_type | Name of slice_type + # 0 | B (B slice) + # 1 | P (P slice) + # 2 | I (I slice) + # unsigned integer 0-th order Exp-Golomb-coded syntax element with the left bit first + slice_type, _ = get_ue(dat, rbsp_start, skip_bits) + if DEBUG: + print(" slice_type:", slice_type, f"(first slice: {is_first_slice})") + if slice_type > 2: + raise VideoFileInvalid("slice_type must be 0, 1, or 2") + return slice_type, is_first_slice + +def hevc_index(hevc_file_name: str, allow_corrupt: bool=False) -> Tuple[list, int, bytes]: + with FileReader(hevc_file_name) as f: + dat = f.read() + + if len(dat) < NAL_UNIT_START_CODE_SIZE + 1: + raise VideoFileInvalid("data is too short") + + if dat[0] != 0x00: + raise VideoFileInvalid("first byte must be 0x00") + + prefix_dat = b"" + frame_types = list() + + i = 1 # skip past first byte 0x00 + try: + while i < len(dat): + require_nal_unit_start(dat, i) + nal_unit_len = get_hevc_nal_unit_length(dat, i) + nal_unit_type = get_hevc_nal_unit_type(dat, i) + if nal_unit_type in HEVC_PARAMETER_SET_NAL_UNITS: + prefix_dat += dat[i:i+nal_unit_len] + elif nal_unit_type in HEVC_CODED_SLICE_SEGMENT_NAL_UNITS: + slice_type, is_first_slice = get_hevc_slice_type(dat, i, nal_unit_type) + if is_first_slice: + frame_types.append((slice_type, i)) + i += nal_unit_len + except Exception as e: + if not allow_corrupt: + raise + print(f"ERROR: NAL unit skipped @ {i}\n", str(e)) + + return frame_types, len(dat), prefix_dat + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("input_file", type=str) + parser.add_argument("output_prefix_file", type=str) + parser.add_argument("output_index_file", type=str) + args = parser.parse_args() + + frame_types, dat_len, prefix_dat = hevc_index(args.input_file) + with open(args.output_prefix_file, "wb") as f: + f.write(prefix_dat) + + with open(args.output_index_file, "wb") as f: + for ft, fp in frame_types: + f.write(struct.pack(" -#include - -static const uint32_t BS_MASKS[33] = { - 0, 0x1L, 0x3L, 0x7L, 0xFL, 0x1FL, - 0x3FL, 0x7FL, 0xFFL, 0x1FFL, 0x3FFL, 0x7FFL, - 0xFFFL, 0x1FFFL, 0x3FFFL, 0x7FFFL, 0xFFFFL, 0x1FFFFL, - 0x3FFFFL, 0x7FFFFL, 0xFFFFFL, 0x1FFFFFL, 0x3FFFFFL, 0x7FFFFFL, - 0xFFFFFFL, 0x1FFFFFFL, 0x3FFFFFFL, 0x7FFFFFFL, 0xFFFFFFFL, 0x1FFFFFFFL, - 0x3FFFFFFFL, 0x7FFFFFFFL, 0xFFFFFFFFL}; - -void bs_init(struct bitstream* bs, const uint8_t* buffer, size_t input_size) { - bs->buffer_ptr = buffer; - bs->buffer_end = buffer + input_size; - bs->value = 0; - bs->pos = 0; - bs->shift = 8; - bs->size = input_size * 8; -} - -uint32_t bs_get(struct bitstream* bs, int n) { - if (n > 32) - return 0; - - bs->pos += n; - bs->shift += n; - while (bs->shift > 8) { - if (bs->buffer_ptr < bs->buffer_end) { - bs->value <<= 8; - bs->value |= *bs->buffer_ptr++; - bs->shift -= 8; - } else { - bs_seek(bs, bs->pos - n); - return 0; - // bs->value <<= 8; - // bs->shift -= 8; - } - } - return (bs->value >> (8 - bs->shift)) & BS_MASKS[n]; -} - -void bs_seek(struct bitstream* bs, size_t new_pos) { - bs->pos = (new_pos / 32) * 32; - bs->shift = 8; - bs->value = 0; - bs_get(bs, new_pos % 32); -} - -uint32_t bs_peek(struct bitstream* bs, int n) { - struct bitstream bak = *bs; - return bs_get(&bak, n); -} - -size_t bs_remain(struct bitstream* bs) { - return bs->size - bs->pos; -} - -int bs_eof(struct bitstream* bs) { - return bs_remain(bs) == 0; -} - -uint32_t bs_ue(struct bitstream* bs) { - static const uint8_t exp_golomb_bits[256] = { - 8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - }; - uint32_t bits, read = 0; - int bits_left; - uint8_t coded; - int done = 0; - bits = 0; - // we want to read 8 bits at a time - if we don't have 8 bits, - // read what's left, and shift. The exp_golomb_bits calc remains the - // same. - while (!done) { - bits_left = bs_remain(bs); - if (bits_left < 8) { - read = bs_peek(bs, bits_left) << (8 - bits_left); - done = 1; - } else { - read = bs_peek(bs, 8); - if (read == 0) { - bs_get(bs, 8); - bits += 8; - } else { - done = 1; - } - } - } - coded = exp_golomb_bits[read]; - bs_get(bs, coded); - bits += coded; - - // printf("ue - bits %d\n", bits); - return bs_get(bs, bits + 1) - 1; -} - -int32_t bs_se(struct bitstream* bs) { - uint32_t ret; - ret = bs_ue(bs); - if ((ret & 0x1) == 0) { - ret >>= 1; - int32_t temp = 0 - ret; - return temp; - } - return (ret + 1) >> 1; -} diff --git a/tools/lib/vidindex/bitstream.h b/tools/lib/vidindex/bitstream.h deleted file mode 100644 index 0f538a59ab..0000000000 --- a/tools/lib/vidindex/bitstream.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef bitstream_H -#define bitstream_H - - -#include -#include - -struct bitstream { - const uint8_t *buffer_ptr; - const uint8_t *buffer_end; - uint64_t value; - uint32_t pos; - uint32_t shift; - size_t size; -}; - -void bs_init(struct bitstream *bs, const uint8_t *buffer, size_t input_size); -void bs_seek(struct bitstream *bs, size_t new_pos); -uint32_t bs_get(struct bitstream *bs, int n); -uint32_t bs_peek(struct bitstream *bs, int n); -size_t bs_remain(struct bitstream *bs); -int bs_eof(struct bitstream *bs); -uint32_t bs_ue(struct bitstream *bs); -int32_t bs_se(struct bitstream *bs); - -#endif diff --git a/tools/lib/vidindex/vidindex.c b/tools/lib/vidindex/vidindex.c deleted file mode 100644 index 1d7da2e193..0000000000 --- a/tools/lib/vidindex/vidindex.c +++ /dev/null @@ -1,307 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "./bitstream.h" - -#define START_CODE 0x000001 - -static uint32_t read24be(const uint8_t* ptr) { - return (ptr[0] << 16) | (ptr[1] << 8) | ptr[2]; -} -static void write32le(FILE *of, uint32_t v) { - uint8_t va[4] = { - v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >> 24) & 0xff - }; - fwrite(va, 1, sizeof(va), of); -} - -// Table 7-1 -enum hevc_nal_type { - HEVC_NAL_TYPE_TRAIL_N = 0, - HEVC_NAL_TYPE_TRAIL_R = 1, - HEVC_NAL_TYPE_TSA_N = 2, - HEVC_NAL_TYPE_TSA_R = 3, - HEVC_NAL_TYPE_STSA_N = 4, - HEVC_NAL_TYPE_STSA_R = 5, - HEVC_NAL_TYPE_RADL_N = 6, - HEVC_NAL_TYPE_RADL_R = 7, - HEVC_NAL_TYPE_RASL_N = 8, - HEVC_NAL_TYPE_RASL_R = 9, - HEVC_NAL_TYPE_BLA_W_LP = 16, - HEVC_NAL_TYPE_BLA_W_RADL = 17, - HEVC_NAL_TYPE_BLA_N_LP = 18, - HEVC_NAL_TYPE_IDR_W_RADL = 19, - HEVC_NAL_TYPE_IDR_N_LP = 20, - HEVC_NAL_TYPE_CRA_NUT = 21, - HEVC_NAL_TYPE_RSV_IRAP_VCL23 = 23, - HEVC_NAL_TYPE_VPS_NUT = 32, - HEVC_NAL_TYPE_SPS_NUT = 33, - HEVC_NAL_TYPE_PPS_NUT = 34, - HEVC_NAL_TYPE_AUD_NUT = 35, - HEVC_NAL_TYPE_EOS_NUT = 36, - HEVC_NAL_TYPE_EOB_NUT = 37, - HEVC_NAL_TYPE_FD_NUT = 38, - HEVC_NAL_TYPE_PREFIX_SEI_NUT = 39, - HEVC_NAL_TYPE_SUFFIX_SEI_NUT = 40, -}; - -// Table 7-7 -enum hevc_slice_type { - HEVC_SLICE_B = 0, - HEVC_SLICE_P = 1, - HEVC_SLICE_I = 2, -}; - -static void hevc_index(const uint8_t *data, size_t file_size, FILE *of_prefix, FILE *of_index) { - const uint8_t* ptr = data; - const uint8_t* ptr_end = data + file_size; - - assert(ptr[0] == 0); - ptr++; - assert(read24be(ptr) == START_CODE); - - // pps. ignore for now - uint32_t num_extra_slice_header_bits = 0; - uint32_t dependent_slice_segments_enabled_flag = 0; - - while (ptr < ptr_end) { - const uint8_t* next = ptr+1; - for (; next < ptr_end-4; next++) { - if (read24be(next) == START_CODE) break; - } - size_t nal_size = next - ptr; - if (nal_size < 6) { - break; - } - - { - struct bitstream bs = {0}; - bs_init(&bs, ptr, nal_size); - - uint32_t start_code = bs_get(&bs, 24); - assert(start_code == 0x000001); - - // nal_unit_header - uint32_t forbidden_zero_bit = bs_get(&bs, 1); - uint32_t nal_unit_type = bs_get(&bs, 6); - uint32_t nuh_layer_id = bs_get(&bs, 6); - uint32_t nuh_temporal_id_plus1 = bs_get(&bs, 3); - - // if (nal_unit_type != 1) printf("%3d -- %3d %10d %lu\n", nal_unit_type, frame_num, (uint32_t)(ptr-data), nal_size); - - switch (nal_unit_type) { - case HEVC_NAL_TYPE_VPS_NUT: - case HEVC_NAL_TYPE_SPS_NUT: - case HEVC_NAL_TYPE_PPS_NUT: - fwrite(ptr, 1, nal_size, of_prefix); - break; - case HEVC_NAL_TYPE_TRAIL_N: - case HEVC_NAL_TYPE_TRAIL_R: - case HEVC_NAL_TYPE_TSA_N: - case HEVC_NAL_TYPE_TSA_R: - case HEVC_NAL_TYPE_STSA_N: - case HEVC_NAL_TYPE_STSA_R: - case HEVC_NAL_TYPE_RADL_N: - case HEVC_NAL_TYPE_RADL_R: - case HEVC_NAL_TYPE_RASL_N: - case HEVC_NAL_TYPE_RASL_R: - case HEVC_NAL_TYPE_BLA_W_LP: - case HEVC_NAL_TYPE_BLA_W_RADL: - case HEVC_NAL_TYPE_BLA_N_LP: - case HEVC_NAL_TYPE_IDR_W_RADL: - case HEVC_NAL_TYPE_IDR_N_LP: - case HEVC_NAL_TYPE_CRA_NUT: { - // slice_segment_header - uint32_t first_slice_segment_in_pic_flag = bs_get(&bs, 1); - if (nal_unit_type >= HEVC_NAL_TYPE_BLA_W_LP && nal_unit_type <= HEVC_NAL_TYPE_RSV_IRAP_VCL23) { - uint32_t no_output_of_prior_pics_flag = bs_get(&bs, 1); - } - uint32_t slice_pic_parameter_set_id = bs_get(&bs, 1); - if (!first_slice_segment_in_pic_flag) { - // ... - break; - } - - if (!dependent_slice_segments_enabled_flag) { - for (int i=0; i 4); - - const uint8_t* data = (const uint8_t*)mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); - assert(data != MAP_FAILED); - - if (strcmp(file_type, "hevc") == 0) { - hevc_index(data, file_size, of_prefix, of_index); - } else if (strcmp(file_type, "h264") == 0) { - h264_index(data, file_size, of_prefix, of_index); - } else { - assert(false); - } - - munmap((void*)data, file_size); - close(fd); - - return 0; -} diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index c767131f51..9ec2097ea4 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -2,6 +2,20 @@ set -e +if [ -z "$SKIP_PROMPT" ]; then + echo "--------------- macOS support ---------------" + echo "Running openpilot natively on macOS is not officially supported." + echo "It might build, some parts of it might work, but it's not fully tested, so there might be some issues." + echo + echo "Check out devcontainers for a seamless experience (see tools/README.md)." + echo "-------------------------------------------------" + echo -n "Are you sure you want to continue? [y/N] " + read -r response + if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + exit 1 + fi +fi + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$(cd $DIR/../ && pwd)" ARCH=$(uname -m) @@ -49,7 +63,7 @@ brew "pyenv" brew "pyenv-virtualenv" brew "qt@5" brew "zeromq" -brew "gcc@12" +brew "gcc@13" cask "gcc-arm-embedded" brew "portaudio" EOS @@ -74,6 +88,27 @@ export PYCURL_SSL_LIBRARY=openssl $DIR/install_python_dependencies.sh echo "[ ] installed python dependencies t=$SECONDS" +# brew does not link qt5 by default +# check if qt5 can be linked, if not, prompt the user to link it +QT_BIN_LOCATION="$(command -v lupdate || :)" +if [ -n "$QT_BIN_LOCATION" ]; then + # if qt6 is linked, prompt the user to unlink it and link the right version + QT_BIN_VERSION="$(lupdate -version | awk '{print $NF}')" + if [[ ! "$QT_BIN_VERSION" =~ 5\.[0-9]+\.[0-9]+ ]]; then + echo + echo "lupdate/lrelease available at PATH is $QT_BIN_VERSION" + if [[ "$QT_BIN_LOCATION" == "$(brew --prefix)/"* ]]; then + echo "Run the following command to link qt5:" + echo "brew unlink qt@6 && brew link qt@5" + else + echo "Remove conflicting qt entries from PATH and run the following command to link qt5:" + echo "brew link qt@5" + fi + fi +else + brew link qt@5 +fi + echo echo "---- OPENPILOT SETUP DONE ----" echo "Open a new shell or configure your active shell env by running:" diff --git a/tools/plotjuggler/layouts/CAN-bus-debug.xml b/tools/plotjuggler/layouts/CAN-bus-debug.xml new file mode 100644 index 0000000000..f5b74ae71f --- /dev/null +++ b/tools/plotjuggler/layouts/CAN-bus-debug.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/plotjuggler/layouts/gps_vs_llk.xml b/tools/plotjuggler/layouts/gps_vs_llk.xml new file mode 100644 index 0000000000..44980712ed --- /dev/null +++ b/tools/plotjuggler/layouts/gps_vs_llk.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + R = 6378.137 -- Radius of earth in KM + -- Compute the Haversine distance between +-- two points defined by latitude and longitude. +-- Return the distance in meters +lat1, lon1 = value, v1 +lat2, lon2 = v2, v3 +dLat = (lat2 - lat1) * math.pi / 180 +dLon = (lon2 - lon1) * math.pi / 180 +a = math.sin(dLat/2) * math.sin(dLat/2) + +math.cos(lat1 * math.pi / 180) * math.cos(lat2 * math.pi / 180) * +math.sin(dLon/2) * math.sin(dLon/2) +c = 2 * math.atan(math.sqrt(a), math.sqrt(1-a)) +d = R * c +distance = d * 1000 -- meters +return distance + /gpsLocationExternal/latitude + + /gpsLocationExternal/longitude + /liveLocationKalman/positionGeodetic/value/0 + /liveLocationKalman/positionGeodetic/value/1 + + + + + + + diff --git a/tools/plotjuggler/layouts/longitudinal.xml b/tools/plotjuggler/layouts/longitudinal.xml index ff8f3ad193..33a24f76d6 100644 --- a/tools/plotjuggler/layouts/longitudinal.xml +++ b/tools/plotjuggler/layouts/longitudinal.xml @@ -3,10 +3,10 @@ - + - + @@ -15,7 +15,16 @@ - + + + + + + + + + + @@ -23,7 +32,7 @@ - + diff --git a/tools/replay/SConscript b/tools/replay/SConscript index bce7512e44..db8447003b 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -1,25 +1,21 @@ -import os -Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', - 'cereal', 'transformations') +Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'cereal') base_frameworks = qt_env['FRAMEWORKS'] -base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', - 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] +base_libs = [common, messaging, cereal, visionipc, 'zmq', + 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread', 'qt_util'] + qt_env["LIBS"] if arch == "Darwin": base_frameworks.append('OpenCL') else: base_libs.append('OpenCL') -qt_libs = ['qt_util'] + base_libs qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc"] - -replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=qt_libs, FRAMEWORKS=base_frameworks) +replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') -replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + qt_libs +replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + base_libs qt_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): - qt_env.Program('tests/test_replay', ['tests/test_runner.cc', 'tests/test_replay.cc'], LIBS=[replay_libs, qt_libs]) + qt_env.Program('tests/test_replay', ['tests/test_runner.cc', 'tests/test_replay.cc'], LIBS=[replay_libs, base_libs]) diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index 719bbd69ca..3332aa9c90 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -29,7 +29,7 @@ const std::initializer_list> keyboard_shortc }, { {"enter", "Enter seek request"}, - {"x", "+/-Replay speed"}, + {"+/-", "Playback speed"}, {"q", "Exit"}, }, }; @@ -331,13 +331,18 @@ void ConsoleUI::handleKey(char c) { refresh(); getch_timer.start(1000, this); - } else if (c == 'x') { - if (replay->hasFlag(REPLAY_FLAG_FULL_SPEED)) { - replay->removeFlag(REPLAY_FLAG_FULL_SPEED); - rWarning("replay at normal speed"); - } else { - replay->addFlag(REPLAY_FLAG_FULL_SPEED); - rWarning("replay at full speed"); + } else if (c == '+' || c == '=') { + auto it = std::upper_bound(speed_array.begin(), speed_array.end(), replay->getSpeed()); + if (it != speed_array.end()) { + rWarning("playback speed: %.1fx", *it); + replay->setSpeed(*it); + } + } else if (c == '_' || c == '-') { + auto it = std::lower_bound(speed_array.begin(), speed_array.end(), replay->getSpeed()); + if (it != speed_array.begin()) { + auto prev = std::prev(it); + rWarning("playback speed: %.1fx", *prev); + replay->setSpeed(*prev); } } else if (c == 'e') { replay->seekToFlag(FindFlag::nextEngagement); diff --git a/tools/replay/consoleui.h b/tools/replay/consoleui.h index 20e07524dd..6ed44bc623 100644 --- a/tools/replay/consoleui.h +++ b/tools/replay/consoleui.h @@ -16,6 +16,7 @@ class ConsoleUI : public QObject { public: ConsoleUI(Replay *replay, QObject *parent = 0); ~ConsoleUI(); + inline static const std::array speed_array = {0.2f, 0.5f, 1.0f, 2.0f, 3.0f}; private: void initWindows(); diff --git a/tools/replay/filereader.cc b/tools/replay/filereader.cc index 88879067c9..22af7f5f86 100644 --- a/tools/replay/filereader.cc +++ b/tools/replay/filereader.cc @@ -3,11 +3,12 @@ #include #include "common/util.h" +#include "system/hardware/hw.h" #include "tools/replay/util.h" std::string cacheFilePath(const std::string &url) { static std::string cache_path = [] { - const std::string comma_cache = util::getenv("COMMA_CACHE", "/tmp/comma_download_cache/"); + const std::string comma_cache = Path::download_cache_root(); util::create_directories(comma_cache, 0755); return comma_cache.back() == '/' ? comma_cache : comma_cache + "/"; }(); diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index 74aebceae5..c92ff4753f 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -1,6 +1,7 @@ #include "tools/replay/logreader.h" #include +#include "tools/replay/filereader.h" #include "tools/replay/util.h" Event::Event(const kj::ArrayPtr &amsg, bool frame) : reader(amsg), frame(frame) { @@ -40,9 +41,7 @@ LogReader::~LogReader() { } } -bool LogReader::load(const std::string &url, std::atomic *abort, - const std::set &allow, - bool local_cache, int chunk_size, int retries) { +bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache, int chunk_size, int retries) { raw_ = FileReader(local_cache, chunk_size, retries).read(url, abort); if (raw_.empty()) return false; @@ -50,15 +49,15 @@ bool LogReader::load(const std::string &url, std::atomic *abort, raw_ = decompressBZ2(raw_, abort); if (raw_.empty()) return false; } - return parse(allow, abort); + return parse(abort); } bool LogReader::load(const std::byte *data, size_t size, std::atomic *abort) { raw_.assign((const char *)data, size); - return parse({}, abort); + return parse(abort); } -bool LogReader::parse(const std::set &allow, std::atomic *abort) { +bool LogReader::parse(std::atomic *abort) { try { kj::ArrayPtr words((const capnp::word *)raw_.data(), raw_.size() / sizeof(capnp::word)); while (words.size() > 0 && !(abort && *abort)) { @@ -67,12 +66,6 @@ bool LogReader::parse(const std::set &allow, std::atomicwhich) == allow.end()) { - words = kj::arrayPtr(evt->reader.getEnd(), words.end()); - delete evt; - continue; - } - // Add encodeIdx packet again as a frame packet for the video stream if (evt->which == cereal::Event::ROAD_ENCODE_IDX || evt->which == cereal::Event::DRIVER_ENCODE_IDX || diff --git a/tools/replay/logreader.h b/tools/replay/logreader.h index 77d751a91b..73f822d16c 100644 --- a/tools/replay/logreader.h +++ b/tools/replay/logreader.h @@ -6,13 +6,11 @@ #endif #include -#include #include #include #include "cereal/gen/cpp/log.capnp.h" #include "system/camerad/cameras/camera_common.h" -#include "tools/replay/filereader.h" const CameraType ALL_CAMERAS[] = {RoadCam, DriverCam, WideRoadCam}; const int MAX_CAMERAS = std::size(ALL_CAMERAS); @@ -55,13 +53,13 @@ class LogReader { public: LogReader(size_t memory_pool_block_size = DEFAULT_EVENT_MEMORY_POOL_BLOCK_SIZE); ~LogReader(); - bool load(const std::string &url, std::atomic *abort = nullptr, const std::set &allow = {}, + bool load(const std::string &url, std::atomic *abort = nullptr, bool local_cache = false, int chunk_size = -1, int retries = 0); bool load(const std::byte *data, size_t size, std::atomic *abort = nullptr); std::vector events; private: - bool parse(const std::set &allow, std::atomic *abort); + bool parse(std::atomic *abort); std::string raw_; #ifdef HAS_MEMORY_RESOURCE std::unique_ptr mbr_; diff --git a/tools/replay/main.cc b/tools/replay/main.cc index 98a0bb3333..a0c072438d 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -13,7 +13,6 @@ int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); - const QStringList base_blacklist = {"uiDebug", "userFlag"}; const std::tuple flags[] = { {"dcam", REPLAY_FLAG_DCAM, "load driver camera"}, {"ecam", REPLAY_FLAG_ECAM, "load wide road camera"}, @@ -22,7 +21,7 @@ int main(int argc, char *argv[]) { {"qcam", REPLAY_FLAG_QCAMERA, "load qcamera"}, {"no-hw-decoder", REPLAY_FLAG_NO_HW_DECODER, "disable HW video decoding"}, {"no-vipc", REPLAY_FLAG_NO_VIPC, "do not output video"}, - {"all", REPLAY_FLAG_ALL_SERVICES, "do output all messages including " + base_blacklist.join(", ") + + {"all", REPLAY_FLAG_ALL_SERVICES, "do output all messages including uiDebug, userFlag" ". this may causes issues when used along with UI"} }; @@ -34,6 +33,8 @@ int main(int argc, char *argv[]) { parser.addOption({{"b", "block"}, "blacklist of services to send", "block"}); parser.addOption({{"c", "cache"}, "cache segments in memory. default is 5", "n"}); parser.addOption({{"s", "start"}, "start from ", "seconds"}); + parser.addOption({"x", QString("playback . between %1 - %2") + .arg(ConsoleUI::speed_array.front()).arg(ConsoleUI::speed_array.back()), "speed"}); parser.addOption({"demo", "use a demo route instead of providing your own"}); parser.addOption({"data_dir", "local directory with routes", "data_dir"}); parser.addOption({"prefix", "set OPENPILOT_PREFIX", "prefix"}); @@ -64,10 +65,14 @@ int main(int argc, char *argv[]) { op_prefix.reset(new OpenpilotPrefix(prefix.toStdString())); } - Replay *replay = new Replay(route, allow, block, base_blacklist, nullptr, replay_flags, parser.value("data_dir"), &app); + Replay *replay = new Replay(route, allow, block, nullptr, replay_flags, parser.value("data_dir"), &app); if (!parser.value("c").isEmpty()) { replay->setSegmentCacheLimit(parser.value("c").toInt()); } + if (!parser.value("x").isEmpty()) { + replay->setSpeed(std::clamp(parser.value("x").toFloat(), + ConsoleUI::speed_array.front(), ConsoleUI::speed_array.back())); + } if (!replay->load()) { return 0; } diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index e323a9f64d..9657375be7 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -7,41 +7,25 @@ #include "cereal/services.h" #include "common/params.h" #include "common/timing.h" -#include "system/hardware/hw.h" #include "tools/replay/util.h" -Replay::Replay(QString route, QStringList allow, QStringList block, QStringList base_blacklist, SubMaster *sm_, uint32_t flags, QString data_dir, QObject *parent) - : sm(sm_), flags_(flags), QObject(parent) { - std::vector s; +Replay::Replay(QString route, QStringList allow, QStringList block, SubMaster *sm_, + uint32_t flags, QString data_dir, QObject *parent) : sm(sm_), flags_(flags), QObject(parent) { + if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) { + block << "uiDebug" << "userFlag"; + } auto event_struct = capnp::Schema::from().asStruct(); sockets_.resize(event_struct.getUnionFields().size()); - for (const auto &it : services) { - auto name = it.second.name.c_str(); - uint16_t which = event_struct.getFieldByName(name).getProto().getDiscriminantValue(); - if ((which == cereal::Event::Which::UI_DEBUG || which == cereal::Event::Which::USER_FLAG) && - !(flags & REPLAY_FLAG_ALL_SERVICES) && - !allow.contains(name)) { - continue; - } - - if ((allow.empty() || allow.contains(name)) && !block.contains(name)) { - sockets_[which] = name; - if (!allow.empty() || !block.empty()) { - allow_list.insert((cereal::Event::Which)which); - } - s.push_back(name); - } - } - - if (!allow_list.empty()) { - // the following events are needed for replay to work properly. - allow_list.insert(cereal::Event::Which::INIT_DATA); - allow_list.insert(cereal::Event::Which::CAR_PARAMS); - if (sockets_[cereal::Event::Which::PANDA_STATES] != nullptr) { - allow_list.insert(cereal::Event::Which::PANDA_STATE_D_E_P_R_E_C_A_T_E_D); + for (const auto &[name, _] : services) { + if (!block.contains(name.c_str()) && (allow.empty() || allow.contains(name.c_str()))) { + uint16_t which = event_struct.getFieldByName(name).getProto().getDiscriminantValue(); + sockets_[which] = name.c_str(); } } + std::vector s; + std::copy_if(sockets_.begin(), sockets_.end(), std::back_inserter(s), + [](const char *name) { return name != nullptr; }); qDebug() << "services " << s; qDebug() << "loading route " << route; @@ -151,13 +135,12 @@ void Replay::buildTimeline() { [(int)cereal::ControlsState::AlertStatus::CRITICAL] = TimelineType::AlertCritical, }; - for (auto it = segments_.cbegin(); it != segments_.cend() && !exit_; ++it) { - LogReader log; - if (!log.load(route_->at(it->first).qlog.toStdString(), &exit_, - {cereal::Event::Which::CONTROLS_STATE, cereal::Event::Which::USER_FLAG}, - !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; + const auto &route_segments = route_->segments(); + for (auto it = route_segments.cbegin(); it != route_segments.cend() && !exit_; ++it) { + std::shared_ptr log(new LogReader()); + if (!log->load(it->second.qlog.toStdString(), &exit_, !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; - for (const Event *e : log.events) { + for (const Event *e : log->events) { if (e->which == cereal::Event::Which::CONTROLS_STATE) { auto cs = e->event.getControlsState(); @@ -185,6 +168,8 @@ void Replay::buildTimeline() { timeline.push_back({toSeconds(e->mono_time), toSeconds(e->mono_time), TimelineType::UserFlag}); } } + std::sort(timeline.begin(), timeline.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); + emit qLogLoaded(it->first, log); } } @@ -227,36 +212,26 @@ void Replay::segmentLoadFinished(bool success) { if (!success) { Segment *seg = qobject_cast(sender()); rWarning("failed to load segment %d, removing it from current replay list", seg->seg_num); - segments_.erase(seg->seg_num); + updateEvents([&]() { + segments_.erase(seg->seg_num); + return true; + }); } queueSegment(); } void Replay::queueSegment() { - if (segments_.empty()) return; - - SegmentMap::iterator begin, cur; - begin = cur = segments_.lower_bound(std::min(current_segment_.load(), segments_.rbegin()->first)); - int distance = std::max(std::ceil(segment_cache_limit / 2.0) - 1, segment_cache_limit - std::distance(cur, segments_.end())); - for (int i = 0; begin != segments_.begin() && i < distance; ++i) { - --begin; - } - auto end = begin; - for (int i = 0; end != segments_.end() && i < segment_cache_limit; ++i) { - ++end; - } + auto cur = segments_.lower_bound(current_segment_.load()); + if (cur == segments_.end()) return; + auto begin = std::prev(cur, std::min(segment_cache_limit / 2, std::distance(segments_.begin(), cur))); + auto end = std::next(begin, std::min(segment_cache_limit, segments_.size())); // load one segment at a time - for (auto it = cur; it != end; ++it) { - auto &[n, seg] = *it; - if ((seg && !seg->isLoaded()) || !seg) { - if (!seg) { - rDebug("loading segment %d...", n); - seg = std::make_unique(n, route_->at(n), flags_, allow_list); - QObject::connect(seg.get(), &Segment::loadFinished, this, &Replay::segmentLoadFinished); - } - break; - } + auto it = std::find_if(cur, end, [](auto &it) { return !it.second || !it.second->isLoaded(); }); + if (it != end && !it->second) { + rDebug("loading segment %d...", it->first); + it->second = std::make_unique(it->first, route_->at(it->first), flags_); + QObject::connect(it->second.get(), &Segment::loadFinished, this, &Replay::segmentLoadFinished); } mergeSegments(begin, end); @@ -293,13 +268,11 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap:: new_events_->clear(); new_events_->reserve(new_events_size); for (int n : segments_need_merge) { - const auto &e = segments_[n]->log->events; - if (e.size() > 0) { - auto insert_from = e.begin(); - if (new_events_->size() > 0 && (*insert_from)->which == cereal::Event::Which::INIT_DATA) ++insert_from; - auto middle = new_events_->insert(new_events_->end(), insert_from, e.end()); - std::inplace_merge(new_events_->begin(), middle, new_events_->end(), Event::lessThan()); - } + size_t size = new_events_->size(); + const auto &events = segments_[n]->log->events; + std::copy_if(events.begin(), events.end(), std::back_inserter(*new_events_), + [this](auto e) { return e->which < sockets_.size() && sockets_[e->which] != nullptr; }); + std::inplace_merge(new_events_->begin(), new_events_->begin() + size, new_events_->end(), Event::lessThan()); } if (stream_thread_) { @@ -317,13 +290,12 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap:: void Replay::startStream(const Segment *cur_segment) { const auto &events = cur_segment->log->events; - // get route start time from initData - auto it = std::find_if(events.begin(), events.end(), [](auto e) { return e->which == cereal::Event::Which::INIT_DATA; }); - route_start_ts_ = it != events.end() ? (*it)->mono_time : events[0]->mono_time; - cur_mono_time_ += route_start_ts_; + // each segment has an INIT_DATA + route_start_ts_ = events.front()->mono_time; + cur_mono_time_ += route_start_ts_ - 1; // write CarParams - it = std::find_if(events.begin(), events.end(), [](auto e) { return e->which == cereal::Event::Which::CAR_PARAMS; }); + auto it = std::find_if(events.begin(), events.end(), [](auto e) { return e->which == cereal::Event::Which::CAR_PARAMS; }); if (it != events.end()) { car_fingerprint_ = (*it)->event.getCarParams().getCarFingerprint(); capnp::MallocMessageBuilder builder; @@ -391,7 +363,7 @@ void Replay::publishFrame(const Event *e) { void Replay::stream() { cereal::Event::Which cur_which = cereal::Event::Which::INIT_DATA; - double prev_replay_speed = 1.0; + double prev_replay_speed = speed_; std::unique_lock lk(stream_lock_); while (true) { @@ -415,35 +387,25 @@ void Replay::stream() { cur_mono_time_ = evt->mono_time; setCurrentSegment(toSeconds(cur_mono_time_) / 60); - // migration for pandaState -> pandaStates to keep UI working for old segments - if (cur_which == cereal::Event::Which::PANDA_STATE_D_E_P_R_E_C_A_T_E_D && - sockets_[cereal::Event::Which::PANDA_STATES] != nullptr) { - MessageBuilder msg; - auto ps = msg.initEvent().initPandaStates(1); - ps[0].setIgnitionLine(true); - ps[0].setPandaType(cereal::PandaState::PandaType::DOS); - pm->send(sockets_[cereal::Event::Which::PANDA_STATES], msg); - } - - if (cur_which < sockets_.size() && sockets_[cur_which] != nullptr) { + if (sockets_[cur_which] != nullptr) { // keep time long etime = (cur_mono_time_ - evt_start_ts) / speed_; long rtime = nanos_since_boot() - loop_start_ts; long behind_ns = etime - rtime; - // if behind_ns is greater than 1 second, it means that an invalid segemnt is skipped by seeking/replaying + // if behind_ns is greater than 1 second, it means that an invalid segment is skipped by seeking/replaying if (behind_ns >= 1 * 1e9 || speed_ != prev_replay_speed) { // reset event start times evt_start_ts = cur_mono_time_; loop_start_ts = nanos_since_boot(); prev_replay_speed = speed_; - } else if (behind_ns > 0 && !hasFlag(REPLAY_FLAG_FULL_SPEED)) { + } else if (behind_ns > 0) { precise_nano_sleep(behind_ns); } if (!evt->frame) { publishMessage(evt); } else if (camera_server_) { - if (hasFlag(REPLAY_FLAG_FULL_SPEED)) { + if (speed_ > 1.0) { camera_server_->waitForSent(); } publishFrame(evt); @@ -456,7 +418,7 @@ void Replay::stream() { } if (eit == events_->end() && !hasFlag(REPLAY_FLAG_NO_LOOP)) { - int last_segment = segments_.rbegin()->first; + int last_segment = segments_.empty() ? 0 : segments_.rbegin()->first; if (current_segment_ >= last_segment && isSegmentMerged(last_segment)) { rInfo("reaches the end of route, restart from beginning"); QMetaObject::invokeMethod(this, std::bind(&Replay::seekTo, this, 0, false), Qt::QueuedConnection); diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 5ed4ff11b5..1a74b69c3d 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -28,7 +27,6 @@ enum REPLAY_FLAGS { REPLAY_FLAG_NO_FILE_CACHE = 0x0020, REPLAY_FLAG_QCAMERA = 0x0040, REPLAY_FLAG_NO_HW_DECODER = 0x0100, - REPLAY_FLAG_FULL_SPEED = 0x0200, REPLAY_FLAG_NO_VIPC = 0x0400, REPLAY_FLAG_ALL_SERVICES = 0x0800, }; @@ -44,13 +42,14 @@ enum class FindFlag { enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserFlag }; typedef bool (*replayEventFilter)(const Event *, void *); +Q_DECLARE_METATYPE(std::shared_ptr); class Replay : public QObject { Q_OBJECT public: - Replay(QString route, QStringList allow, QStringList block, QStringList base_blacklist, SubMaster *sm = nullptr, - uint32_t flags = REPLAY_FLAG_NONE, QString data_dir = "", QObject *parent = 0); + Replay(QString route, QStringList allow, QStringList block, SubMaster *sm = nullptr, + uint32_t flags = REPLAY_FLAG_NONE, QString data_dir = "", QObject *parent = 0); ~Replay(); bool load(); void start(int seconds = 0); @@ -91,6 +90,7 @@ signals: void streamStarted(); void segmentsMerged(); void seekedTo(double sec); + void qLogLoaded(int segnum, std::shared_ptr qlog); protected slots: void segmentLoadFinished(bool success); @@ -112,8 +112,6 @@ protected: } QThread *stream_thread_ = nullptr; - - // logs std::mutex stream_lock_; std::condition_variable stream_cv_; std::atomic updating_events_ = false; @@ -140,9 +138,8 @@ protected: std::mutex timeline_lock; QFuture timeline_future; std::vector> timeline; - std::set allow_list; std::string car_fingerprint_; - float speed_ = 1.0; + std::atomic speed_ = 1.0; replayEventFilter event_filter = nullptr; void *filter_opaque = nullptr; int segment_cache_limit = MIN_SEGMENTS_CACHE; diff --git a/tools/replay/route.cc b/tools/replay/route.cc index 15a57e5e42..d0ddf7f3c8 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -7,9 +7,6 @@ #include #include #include -#include -#include -#include #include "selfdrive/ui/qt/api.h" #include "system/hardware/hw.h" @@ -47,7 +44,7 @@ bool Route::loadFromServer() { loop.exit(success ? loadFromJson(json) : 0); }); - http.sendRequest("https://api.commadotai.com/v1/route/" + route_.str + "/files"); + http.sendRequest(CommaApi::BASE_URL + "/v1/route/" + route_.str + "/files"); return loop.exec(); } @@ -102,9 +99,7 @@ void Route::addFileToSegment(int n, const QString &file) { // class Segment -Segment::Segment(int n, const SegmentFile &files, uint32_t flags, - const std::set &allow) - : seg_num(n), flags(flags), allow(allow) { +Segment::Segment(int n, const SegmentFile &files, uint32_t flags) : seg_num(n), flags(flags) { // [RoadCam, DriverCam, WideRoadCam, log]. fallback to qcamera/qlog const std::array file_list = { (flags & REPLAY_FLAG_QCAMERA) || files.road_cam.isEmpty() ? files.qcamera : files.road_cam, @@ -135,7 +130,7 @@ void Segment::loadFile(int id, const std::string file) { success = frames[id]->load(file, flags & REPLAY_FLAG_NO_HW_DECODER, &abort_, local_cache, 20 * 1024 * 1024, 3); } else { log = std::make_unique(); - success = log->load(file, &abort_, allow, local_cache, 0, 3); + success = log->load(file, &abort_, local_cache, 0, 3); } if (!success) { diff --git a/tools/replay/route.h b/tools/replay/route.h index 7207ff4f5c..4dad7a1f37 100644 --- a/tools/replay/route.h +++ b/tools/replay/route.h @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -55,7 +54,7 @@ class Segment : public QObject { Q_OBJECT public: - Segment(int n, const SegmentFile &files, uint32_t flags, const std::set &allow = {}); + Segment(int n, const SegmentFile &files, uint32_t flags); ~Segment(); inline bool isLoaded() const { return !loading_ && !abort_; } @@ -73,5 +72,4 @@ protected: std::atomic loading_ = 0; QFutureSynchronizer synchronizer_; uint32_t flags; - std::set allow; }; diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index 393a561c0f..1873daaf4b 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -12,7 +12,7 @@ const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; const std::string TEST_RLOG_CHECKSUM = "5b966d4bb21a100a8c4e59195faeb741b975ccbe268211765efd1763d892bfb3"; -bool donload_to_file(const std::string &url, const std::string &local_file, int chunk_size = 5 * 1024 * 1024, int retries = 3) { +bool download_to_file(const std::string &url, const std::string &local_file, int chunk_size = 5 * 1024 * 1024, int retries = 3) { do { if (httpDownload(url, local_file, chunk_size)) { return true; @@ -29,7 +29,7 @@ TEST_CASE("httpMultiPartDownload") { const size_t chunk_size = 5 * 1024 * 1024; std::string content; SECTION("download to file") { - REQUIRE(donload_to_file(TEST_RLOG_URL, filename, chunk_size)); + REQUIRE(download_to_file(TEST_RLOG_URL, filename, chunk_size)); content = util::read_file(filename); } SECTION("download to buffer") { @@ -110,47 +110,58 @@ void read_segment(int n, const SegmentFile &segment_file, uint32_t flags) { loop.exec(); } -TEST_CASE("Route") { - // Create a local route from remote for testing - Route remote_route(DEMO_ROUTE); - REQUIRE(remote_route.load()); - char tmp_path[] = "/tmp/root_XXXXXX"; - const std::string data_dir = mkdtemp(tmp_path); - const std::string route_name = DEMO_ROUTE.mid(17).toStdString(); - for (int i = 0; i < 2; ++i) { - std::string log_path = util::string_format("%s/%s--%d/", data_dir.c_str(), route_name.c_str(), i); - util::create_directories(log_path, 0755); - REQUIRE(donload_to_file(remote_route.at(i).rlog.toStdString(), log_path + "rlog.bz2")); - REQUIRE(donload_to_file(remote_route.at(i).road_cam.toStdString(), log_path + "fcamera.hevc")); - REQUIRE(donload_to_file(remote_route.at(i).driver_cam.toStdString(), log_path + "dcamera.hevc")); - REQUIRE(donload_to_file(remote_route.at(i).wide_road_cam.toStdString(), log_path + "ecamera.hevc")); - REQUIRE(donload_to_file(remote_route.at(i).qcamera.toStdString(), log_path + "qcamera.ts")); - } +std::string download_demo_route() { + static std::string data_dir; - SECTION("Local route") { - auto flags = GENERATE(REPLAY_FLAG_DCAM | REPLAY_FLAG_ECAM, REPLAY_FLAG_QCAMERA); - Route route(DEMO_ROUTE, QString::fromStdString(data_dir)); - REQUIRE(route.load()); - REQUIRE(route.segments().size() == 2); - for (int i = 0; i < route.segments().size(); ++i) { - read_segment(i, route.at(i), flags); - } - }; - SECTION("Remote route") { - auto flags = GENERATE(REPLAY_FLAG_DCAM | REPLAY_FLAG_ECAM, REPLAY_FLAG_QCAMERA); - Route route(DEMO_ROUTE); - REQUIRE(route.load()); - REQUIRE(route.segments().size() == 13); + if (data_dir == "") { + char tmp_path[] = "/tmp/root_XXXXXX"; + data_dir = mkdtemp(tmp_path); + + Route remote_route(DEMO_ROUTE); + assert(remote_route.load()); + + // Create a local route from remote for testing + const std::string route_name = DEMO_ROUTE.mid(17).toStdString(); for (int i = 0; i < 2; ++i) { - read_segment(i, route.at(i), flags); + std::string log_path = util::string_format("%s/%s--%d/", data_dir.c_str(), route_name.c_str(), i); + util::create_directories(log_path, 0755); + REQUIRE(download_to_file(remote_route.at(i).rlog.toStdString(), log_path + "rlog.bz2")); + REQUIRE(download_to_file(remote_route.at(i).driver_cam.toStdString(), log_path + "dcamera.hevc")); + REQUIRE(download_to_file(remote_route.at(i).wide_road_cam.toStdString(), log_path + "ecamera.hevc")); + REQUIRE(download_to_file(remote_route.at(i).qcamera.toStdString(), log_path + "qcamera.ts")); } - }; + } + + return data_dir; +} + + +TEST_CASE("Local route") { + std::string data_dir = download_demo_route(); + + auto flags = GENERATE(REPLAY_FLAG_DCAM | REPLAY_FLAG_ECAM, REPLAY_FLAG_QCAMERA); + Route route(DEMO_ROUTE, QString::fromStdString(data_dir)); + REQUIRE(route.load()); + REQUIRE(route.segments().size() == 2); + for (int i = 0; i < route.segments().size(); ++i) { + read_segment(i, route.at(i), flags); + } +} + +TEST_CASE("Remote route") { + auto flags = GENERATE(REPLAY_FLAG_DCAM | REPLAY_FLAG_ECAM, REPLAY_FLAG_QCAMERA); + Route route(DEMO_ROUTE); + REQUIRE(route.load()); + REQUIRE(route.segments().size() == 13); + for (int i = 0; i < 2; ++i) { + read_segment(i, route.at(i), flags); + } } // helper class for unit tests class TestReplay : public Replay { public: - TestReplay(const QString &route, uint32_t flags = REPLAY_FLAG_NO_FILE_CACHE | REPLAY_FLAG_NO_VIPC) : Replay(route, {}, {}, {}, nullptr, flags) {} + TestReplay(const QString &route, uint32_t flags = REPLAY_FLAG_NO_FILE_CACHE | REPLAY_FLAG_NO_VIPC) : Replay(route, {}, {}, nullptr, flags) {} void test_seek(); void testSeekTo(int seek_to); }; diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 43195a481d..05e8bdec1f 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -116,9 +116,9 @@ def ui_thread(addr): if yuv_img_raw is None or not yuv_img_raw.data.any(): continue - imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8).reshape((vipc_client.height * 3 // 2, vipc_client.width)) + imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8).reshape((len(yuv_img_raw.data) // vipc_client.stride, vipc_client.stride)) num_px = vipc_client.width * vipc_client.height - bgr = cv2.cvtColor(imgff, cv2.COLOR_YUV2RGB_NV12) + bgr = cv2.cvtColor(imgff[:vipc_client.height * 3 // 2, :vipc_client.width], cv2.COLOR_YUV2RGB_NV12) zoom_matrix = _BB_TO_FULL_FRAME[num_px] cv2.warpAffine(bgr, zoom_matrix[:2], (img.shape[1], img.shape[0]), dst=img, flags=cv2.WARP_INVERSE_MAP) diff --git a/tools/replay/util.cc b/tools/replay/util.cc index 172d545c75..b4f72d0530 100644 --- a/tools/replay/util.cc +++ b/tools/replay/util.cc @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -18,10 +19,7 @@ #include "common/util.h" ReplayMessageHandler message_handler = nullptr; -DownloadProgressHandler download_progress_handler = nullptr; - void installMessageHandler(ReplayMessageHandler handler) { message_handler = handler; } -void installDownloadProgressHandler(DownloadProgressHandler handler) { download_progress_handler = handler; } void logMessage(ReplyMsgType type, const char *fmt, ...) { static std::mutex lock; @@ -93,6 +91,11 @@ size_t write_cb(char *data, size_t size, size_t count, void *userp) { size_t dumy_write_cb(char *data, size_t size, size_t count, void *userp) { return size * count; } struct DownloadStats { + void installDownloadProgressHandler(DownloadProgressHandler handler) { + std::lock_guard lk(lock); + download_progress_handler = handler; + } + void add(const std::string &url, uint64_t total_bytes) { std::lock_guard lk(lock); items[url] = {0, total_bytes}; @@ -120,10 +123,17 @@ struct DownloadStats { std::mutex lock; std::map> items; double prev_tm = 0; + DownloadProgressHandler download_progress_handler = nullptr; }; +static DownloadStats download_stats; + } // namespace +void installDownloadProgressHandler(DownloadProgressHandler handler) { + download_stats.installDownloadProgressHandler(handler); +} + std::string formattedDataSize(size_t size) { if (size < 1024) { return std::to_string(size) + " B"; @@ -166,7 +176,6 @@ std::string getUrlWithoutQuery(const std::string &url) { template bool httpDownload(const std::string &url, T &buf, size_t chunk_size, size_t content_length, std::atomic *abort) { - static DownloadStats download_stats; download_stats.add(url, content_length); int parts = 1; diff --git a/tools/scripts/fetch_image_from_route.py b/tools/scripts/fetch_image_from_route.py index 471db7ee3e..b59e7c895e 100755 --- a/tools/scripts/fetch_image_from_route.py +++ b/tools/scripts/fetch_image_from_route.py @@ -2,10 +2,16 @@ import sys if len(sys.argv) < 4: - print(f"{sys.argv[0]} ") - print('example: ./fetch_image_from_route.py "02c45f73a2e5c6e9|2020-06-01--18-03-08" 3 500') + print(f"{sys.argv[0]} [front|wide|driver]") + print('example: ./fetch_image_from_route.py "02c45f73a2e5c6e9|2020-06-01--18-03-08" 3 500 driver') exit(0) +cameras = { + "front": "cameras", + "wide": "ecameras", + "driver": "dcameras" +} + import requests from PIL import Image from openpilot.tools.lib.auth_config import get_token @@ -16,22 +22,22 @@ jwt = get_token() route = sys.argv[1] segment = int(sys.argv[2]) frame = int(sys.argv[3]) +camera = cameras[sys.argv[4]] if len(sys.argv) > 4 and sys.argv[4] in cameras else "cameras" -url = 'https://api.commadotai.com/v1/route/'+sys.argv[1]+"/files" -r = requests.get(url, headers={"Authorization": "JWT "+jwt}, timeout=10) +url = f'https://api.commadotai.com/v1/route/{route}/files' +r = requests.get(url, headers={"Authorization": f"JWT {jwt}"}, timeout=10) assert r.status_code == 200 print("got api response") -cameras = r.json()['cameras'] -if segment >= len(cameras): - raise Exception("segment %d not found, got %d segments" % (segment, len(cameras))) +segments = r.json()[camera] +if segment >= len(segments): + raise Exception("segment %d not found, got %d segments" % (segment, len(segments))) -fr = FrameReader(cameras[segment]) +fr = FrameReader(segments[segment]) if frame >= fr.frame_count: raise Exception("frame %d not found, got %d frames" % (frame, fr.frame_count)) im = Image.fromarray(fr.get(frame, count=1, pix_fmt="rgb24")[0]) -fn = "uxxx_"+route.replace("|", "_")+"_%d_%d.png" % (segment, frame) +fn = f"uxxx_{route.replace('|', '_')}_{segment}_{frame}.png" im.save(fn) print(f"saved {fn}") - diff --git a/tools/scripts/save_ubloxraw_stream.py b/tools/scripts/save_ubloxraw_stream.py old mode 100644 new mode 100755 diff --git a/tools/sim/Dockerfile.sim b/tools/sim/Dockerfile.sim index 9fd8a56101..a183002589 100644 --- a/tools/sim/Dockerfile.sim +++ b/tools/sim/Dockerfile.sim @@ -22,8 +22,7 @@ COPY ./body ${OPENPILOT_PATH}/body COPY ./third_party ${OPENPILOT_PATH}/third_party COPY ./site_scons ${OPENPILOT_PATH}/site_scons COPY ./rednose ${OPENPILOT_PATH}/rednose -COPY ./laika_repo ${OPENPILOT_PATH}/laika_repo -RUN ln -s ${OPENPILOT_PATH}/laika_repo/laika/ ${OPENPILOT_PATH}/laika +COPY ./rednose_repo/site_scons ${OPENPILOT_PATH}/rednose_repo/site_scons COPY ./common ${OPENPILOT_PATH}/common COPY ./opendbc ${OPENPILOT_PATH}/opendbc COPY ./cereal ${OPENPILOT_PATH}/cereal @@ -31,6 +30,7 @@ COPY ./panda ${OPENPILOT_PATH}/panda COPY ./selfdrive ${OPENPILOT_PATH}/selfdrive COPY ./system ${OPENPILOT_PATH}/system COPY ./tools ${OPENPILOT_PATH}/tools +COPY ./release ${OPENPILOT_PATH}/release RUN --mount=type=bind,source=.ci_cache/scons_cache,target=/tmp/scons_cache,rw scons -j$(nproc) --cache-readonly diff --git a/tools/sim/README.md b/tools/sim/README.md index 69a89aefab..0892e77736 100644 --- a/tools/sim/README.md +++ b/tools/sim/README.md @@ -1,58 +1,82 @@ openpilot in simulator ===================== -openpilot implements a [bridge](bridge.py) that allows it to run in the [CARLA simulator](https://carla.org/). +openpilot implements a [bridge](run_bridge.py) that allows it to run in the [MetaDrive simulator](https://github.com/metadriverse/metadrive) or [CARLA simulator](https://carla.org/). -## System Requirements +## Launching openpilot +First, start openpilot. +``` bash +# Run locally +./tools/sim/launch_openpilot.sh +``` + +## Bridge usage +``` +$ ./run_bridge.py -h +usage: run_bridge.py [-h] [--joystick] [--high_quality] [--dual_camera] [--simulator SIMULATOR] [--town TOWN] [--spawn_point NUM_SELECTED_SPAWN_POINT] [--host HOST] [--port PORT] + +Bridge between the simulator and openpilot. + +options: + -h, --help show this help message and exit + --joystick + --high_quality + --dual_camera + --simulator SIMULATOR + --town TOWN + --spawn_point NUM_SELECTED_SPAWN_POINT + --host HOST + --port PORT +``` + +#### Bridge Controls: +- To engage openpilot press 2, then press 1 to increase the speed and 2 to decrease. +- To disengage, press "S" (simulates a user brake) + +#### All inputs: + +``` +| key | functionality | +|------|-----------------------| +| 1 | Cruise Resume / Accel | +| 2 | Cruise Set / Decel | +| 3 | Cruise Cancel | +| r | Reset Simulation | +| i | Toggle Ignition | +| q | Exit all | +| wasd | Control manually | +``` + +## MetaDrive + +### Launching Metadrive +Start bridge processes located in tools/sim: +``` bash +./run_bridge.py --simulator metadrive +``` + +## Carla + +CARLA is also partially supported, though the performance is not great. It needs to be manually installed with: + +```bash +poetry install --with=carla +``` openpilot doesn't have any extreme hardware requirements, however CARLA requires an NVIDIA graphics card and is very resource-intensive and may not run smoothly on your system. For this case, we have the simulator in low quality by default. You can also check out the [CARLA python documentation](https://carla.readthedocs.io/en/latest/python_api/) to find more parameters to tune that might increase performance on your system. -## Running the simulator -Start Carla simulator, openpilot and bridge processes located in tools/sim: +### Launching Carla +Start Carla simulator and bridge processes located in tools/sim: ``` bash # Terminal 1 ./start_carla.sh -# Terminal 2 - Run openpilot and bridge in one Docker: -./start_openpilot_docker.sh - -# Running the latest local code execute - # Terminal 2: - ./launch_openpilot.sh - # Terminal 3 - ./bridge.py -``` - -### Bridge usage -_Same commands hold for start_openpilot_docker_ +# Terminal 2 +./run_bridge.py --simulator carla ``` -$ ./bridge.py -h -Usage: bridge.py [options] -Bridge between CARLA and openpilot. - -Options: - -h, --help show this help message and exit - --joystick Use joystick input to control the car - --high_quality Set simulator to higher quality (requires good GPU) - --town TOWN Select map to drive in - --spawn_point NUM Number of the spawn point to start in - --host HOST Host address of Carla client (127.0.0.1 as default) - --port PORT Port of Carla client (2000 as default) -``` - -To engage openpilot press 1 a few times while focused on bridge.py to increase the cruise speed. -All inputs: - -| key | functionality | -|:----:|:-----------------:| -| 1 | Cruise up 1 mph | -| 2 | Cruise down 1 mph | -| 3 | Cruise cancel | -| q | Exit all | -| wasd | Control manually | ## Further Reading diff --git a/tools/sim/bridge.py b/tools/sim/bridge.py deleted file mode 100755 index 33fb2c020e..0000000000 --- a/tools/sim/bridge.py +++ /dev/null @@ -1,561 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import math -import os -import signal -import threading -import time -from multiprocessing import Process, Queue -from typing import Any - -import carla -import numpy as np -import pyopencl as cl -import pyopencl.array as cl_array - -import cereal.messaging as messaging -from cereal import log -from cereal.visionipc import VisionIpcServer, VisionStreamType -from openpilot.common.basedir import BASEDIR -from openpilot.common.numpy_fast import clip -from openpilot.common.params import Params -from openpilot.common.realtime import DT_DMON, Ratekeeper -from openpilot.selfdrive.car.honda.values import CruiseButtons -from openpilot.selfdrive.test.helpers import set_params_enabled -from openpilot.tools.sim.lib.can import can_function - -W, H = 1928, 1208 -REPEAT_COUNTER = 5 -PRINT_DECIMATION = 100 -STEER_RATIO = 15. - -pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'accelerometer', 'gyroscope', 'can', "gpsLocationExternal"]) -sm = messaging.SubMaster(['carControl', 'controlsState']) - -def parse_args(add_args=None): - parser = argparse.ArgumentParser(description='Bridge between CARLA and openpilot.') - parser.add_argument('--joystick', action='store_true') - parser.add_argument('--high_quality', action='store_true') - parser.add_argument('--dual_camera', action='store_true') - parser.add_argument('--town', type=str, default='Town04_Opt') - parser.add_argument('--spawn_point', dest='num_selected_spawn_point', type=int, default=16) - parser.add_argument('--host', dest='host', type=str, default='127.0.0.1') - parser.add_argument('--port', dest='port', type=int, default=2000) - - return parser.parse_args(add_args) - - -class VehicleState: - def __init__(self): - self.speed = 0.0 - self.angle = 0.0 - self.bearing_deg = 0.0 - self.vel = carla.Vector3D() - self.cruise_button = 0 - self.is_engaged = False - self.ignition = True - - -def steer_rate_limit(old, new): - # Rate limiting to 0.5 degrees per step - limit = 0.5 - if new > old + limit: - return old + limit - elif new < old - limit: - return old - limit - else: - return new - - -class Camerad: - def __init__(self, dual_camera): - self.frame_road_id = 0 - self.frame_wide_id = 0 - self.vipc_server = VisionIpcServer("camerad") - - self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, False, W, H) - if dual_camera: - self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 5, False, W, H) - self.vipc_server.start_listener() - - # set up for pyopencl rgb to yuv conversion - self.ctx = cl.create_some_context() - self.queue = cl.CommandQueue(self.ctx) - cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG " - - kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl") - with open(kernel_fn) as f: - prg = cl.Program(self.ctx, f.read()).build(cl_arg) - self.krnl = prg.rgb_to_nv12 - self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4 - self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4 - - def cam_callback_road(self, image): - self._cam_callback(image, self.frame_road_id, 'roadCameraState', VisionStreamType.VISION_STREAM_ROAD) - self.frame_road_id += 1 - - def cam_callback_wide_road(self, image): - self._cam_callback(image, self.frame_wide_id, 'wideRoadCameraState', VisionStreamType.VISION_STREAM_WIDE_ROAD) - self.frame_wide_id += 1 - - def _cam_callback(self, image, frame_id, pub_type, yuv_type): - img = np.frombuffer(image.raw_data, dtype=np.dtype("uint8")) - img = np.reshape(img, (H, W, 4)) - img = img[:, :, [0, 1, 2]].copy() - - # convert RGB frame to YUV - rgb = np.reshape(img, (H, W * 3)) - rgb_cl = cl_array.to_device(self.queue, rgb) - yuv_cl = cl_array.empty_like(rgb_cl) - self.krnl(self.queue, (np.int32(self.Wdiv4), np.int32(self.Hdiv4)), None, rgb_cl.data, yuv_cl.data).wait() - yuv = np.resize(yuv_cl.get(), rgb.size // 2) - eof = int(frame_id * 0.05 * 1e9) - - self.vipc_server.send(yuv_type, yuv.data.tobytes(), frame_id, eof, eof) - - dat = messaging.new_message(pub_type) - msg = { - "frameId": frame_id, - "transform": [1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0] - } - setattr(dat, pub_type, msg) - pm.send(pub_type, dat) - -def imu_callback(imu, vehicle_state): - # send 5x since 'sensor_tick' doesn't seem to work. limited by the world tick? - for _ in range(5): - vehicle_state.bearing_deg = math.degrees(imu.compass) - dat = messaging.new_message('accelerometer') - dat.accelerometer.sensor = 4 - dat.accelerometer.type = 0x10 - dat.accelerometer.timestamp = dat.logMonoTime # TODO: use the IMU timestamp - dat.accelerometer.init('acceleration') - dat.accelerometer.acceleration.v = [imu.accelerometer.x, imu.accelerometer.y, imu.accelerometer.z] - pm.send('accelerometer', dat) - - # copied these numbers from locationd - dat = messaging.new_message('gyroscope') - dat.gyroscope.sensor = 5 - dat.gyroscope.type = 0x10 - dat.gyroscope.timestamp = dat.logMonoTime # TODO: use the IMU timestamp - dat.gyroscope.init('gyroUncalibrated') - dat.gyroscope.gyroUncalibrated.v = [imu.gyroscope.x, imu.gyroscope.y, imu.gyroscope.z] - pm.send('gyroscope', dat) - time.sleep(0.01) - - -def panda_state_function(vs: VehicleState, exit_event: threading.Event): - pm = messaging.PubMaster(['pandaStates']) - while not exit_event.is_set(): - dat = messaging.new_message('pandaStates', 1) - dat.valid = True - dat.pandaStates[0] = { - 'ignitionLine': vs.ignition, - 'pandaType': "blackPanda", - 'controlsAllowed': True, - 'safetyModel': 'hondaNidec' - } - pm.send('pandaStates', dat) - time.sleep(0.5) - - -def peripheral_state_function(exit_event: threading.Event): - pm = messaging.PubMaster(['peripheralState']) - while not exit_event.is_set(): - dat = messaging.new_message('peripheralState') - dat.valid = True - # fake peripheral state data - dat.peripheralState = { - 'pandaType': log.PandaState.PandaType.blackPanda, - 'voltage': 12000, - 'current': 5678, - 'fanSpeedRpm': 1000 - } - pm.send('peripheralState', dat) - time.sleep(0.5) - - -def gps_callback(gps, vehicle_state): - dat = messaging.new_message('gpsLocationExternal') - - # transform vel from carla to NED - # north is -Y in CARLA - velNED = [ - -vehicle_state.vel.y, # north/south component of NED is negative when moving south - vehicle_state.vel.x, # positive when moving east, which is x in carla - vehicle_state.vel.z, - ] - - dat.gpsLocationExternal = { - "unixTimestampMillis": int(time.time() * 1000), - "flags": 1, # valid fix - "accuracy": 1.0, - "verticalAccuracy": 1.0, - "speedAccuracy": 0.1, - "bearingAccuracyDeg": 0.1, - "vNED": velNED, - "bearingDeg": vehicle_state.bearing_deg, - "latitude": gps.latitude, - "longitude": gps.longitude, - "altitude": gps.altitude, - "speed": vehicle_state.speed, - "source": log.GpsLocationData.SensorSource.ublox, - } - - pm.send('gpsLocationExternal', dat) - - -def fake_driver_monitoring(exit_event: threading.Event): - pm = messaging.PubMaster(['driverStateV2', 'driverMonitoringState']) - while not exit_event.is_set(): - # dmonitoringmodeld output - dat = messaging.new_message('driverStateV2') - dat.driverStateV2.leftDriverData.faceOrientation = [0., 0., 0.] - dat.driverStateV2.leftDriverData.faceProb = 1.0 - dat.driverStateV2.rightDriverData.faceOrientation = [0., 0., 0.] - dat.driverStateV2.rightDriverData.faceProb = 1.0 - pm.send('driverStateV2', dat) - - # dmonitoringd output - dat = messaging.new_message('driverMonitoringState') - dat.driverMonitoringState = { - "faceDetected": True, - "isDistracted": False, - "awarenessStatus": 1., - } - pm.send('driverMonitoringState', dat) - - time.sleep(DT_DMON) - - -def can_function_runner(vs: VehicleState, exit_event: threading.Event): - i = 0 - while not exit_event.is_set(): - can_function(pm, vs.speed, vs.angle, i, vs.cruise_button, vs.is_engaged) - time.sleep(0.01) - i += 1 - - -def connect_carla_client(host: str, port: int): - client = carla.Client(host, port) - client.set_timeout(5) - return client - - -class CarlaBridge: - - def __init__(self, arguments): - set_params_enabled() - - self.params = Params() - - msg = messaging.new_message('liveCalibration') - msg.liveCalibration.validBlocks = 20 - msg.liveCalibration.rpyCalib = [0.0, 0.0, 0.0] - self.params.put("CalibrationParams", msg.to_bytes()) - self.params.put_bool("DisengageOnAccelerator", True) - - self._args = arguments - self._carla_objects = [] - self._camerad = None - self._exit_event = threading.Event() - self._threads = [] - self._keep_alive = True - self.started = False - signal.signal(signal.SIGTERM, self._on_shutdown) - self._exit = threading.Event() - - def _on_shutdown(self, signal, frame): - self._keep_alive = False - - def bridge_keep_alive(self, q: Queue, retries: int): - try: - while self._keep_alive: - try: - self._run(q) - break - except RuntimeError as e: - self.close() - if retries == 0: - raise - - # Reset for another try - self._carla_objects = [] - self._threads = [] - self._exit_event = threading.Event() - - retries -= 1 - if retries <= -1: - print(f"Restarting bridge. Error: {e} ") - else: - print(f"Restarting bridge. Retries left {retries}. Error: {e} ") - finally: - # Clean up resources in the opposite order they were created. - self.close() - - def _run(self, q: Queue): - client = connect_carla_client(self._args.host, self._args.port) - world = client.load_world(self._args.town) - - settings = world.get_settings() - settings.synchronous_mode = True # Enables synchronous mode - settings.fixed_delta_seconds = 0.05 - world.apply_settings(settings) - - world.set_weather(carla.WeatherParameters.ClearSunset) - - if not self._args.high_quality: - world.unload_map_layer(carla.MapLayer.Foliage) - world.unload_map_layer(carla.MapLayer.Buildings) - world.unload_map_layer(carla.MapLayer.ParkedVehicles) - world.unload_map_layer(carla.MapLayer.Props) - world.unload_map_layer(carla.MapLayer.StreetLights) - world.unload_map_layer(carla.MapLayer.Particles) - - blueprint_library = world.get_blueprint_library() - - world_map = world.get_map() - - vehicle_bp = blueprint_library.filter('vehicle.tesla.*')[1] - vehicle_bp.set_attribute('role_name', 'hero') - spawn_points = world_map.get_spawn_points() - assert len(spawn_points) > self._args.num_selected_spawn_point, f'''No spawn point {self._args.num_selected_spawn_point}, try a value between 0 and - {len(spawn_points)} for this town.''' - spawn_point = spawn_points[self._args.num_selected_spawn_point] - vehicle = world.spawn_actor(vehicle_bp, spawn_point) - self._carla_objects.append(vehicle) - max_steer_angle = vehicle.get_physics_control().wheels[0].max_steer_angle - - # make tires less slippery - # wheel_control = carla.WheelPhysicsControl(tire_friction=5) - physics_control = vehicle.get_physics_control() - physics_control.mass = 2326 - # physics_control.wheels = [wheel_control]*4 - physics_control.torque_curve = [[20.0, 500.0], [5000.0, 500.0]] - physics_control.gear_switch_time = 0.0 - vehicle.apply_physics_control(physics_control) - - transform = carla.Transform(carla.Location(x=0.8, z=1.13)) - - def create_camera(fov, callback): - blueprint = blueprint_library.find('sensor.camera.rgb') - blueprint.set_attribute('image_size_x', str(W)) - blueprint.set_attribute('image_size_y', str(H)) - blueprint.set_attribute('fov', str(fov)) - if not self._args.high_quality: - blueprint.set_attribute('enable_postprocess_effects', 'False') - camera = world.spawn_actor(blueprint, transform, attach_to=vehicle) - camera.listen(callback) - return camera - - self._camerad = Camerad(self._args.dual_camera) - - if self._args.dual_camera: - road_wide_camera = create_camera(fov=120, callback=self._camerad.cam_callback_wide_road) # fov bigger than 120 shows unwanted artifacts - self._carla_objects.append(road_wide_camera) - road_camera = create_camera(fov=40, callback=self._camerad.cam_callback_road) - self._carla_objects.append(road_camera) - - vehicle_state = VehicleState() - - # re-enable IMU - imu_bp = blueprint_library.find('sensor.other.imu') - imu_bp.set_attribute('sensor_tick', '0.01') - imu = world.spawn_actor(imu_bp, transform, attach_to=vehicle) - imu.listen(lambda imu: imu_callback(imu, vehicle_state)) - - gps_bp = blueprint_library.find('sensor.other.gnss') - gps = world.spawn_actor(gps_bp, transform, attach_to=vehicle) - gps.listen(lambda gps: gps_callback(gps, vehicle_state)) - self.params.put_bool("UbloxAvailable", True) - - self._carla_objects.extend([imu, gps]) - # launch fake car threads - self._threads.append(threading.Thread(target=panda_state_function, args=(vehicle_state, self._exit_event,))) - self._threads.append(threading.Thread(target=peripheral_state_function, args=(self._exit_event,))) - self._threads.append(threading.Thread(target=fake_driver_monitoring, args=(self._exit_event,))) - self._threads.append(threading.Thread(target=can_function_runner, args=(vehicle_state, self._exit_event,))) - for t in self._threads: - t.start() - - # init - throttle_ease_out_counter = REPEAT_COUNTER - brake_ease_out_counter = REPEAT_COUNTER - steer_ease_out_counter = REPEAT_COUNTER - - vc = carla.VehicleControl(throttle=0, steer=0, brake=0, reverse=False) - - is_openpilot_engaged = False - throttle_out = steer_out = brake_out = 0. - throttle_op = steer_op = brake_op = 0. - throttle_manual = steer_manual = brake_manual = 0. - - old_steer = old_brake = old_throttle = 0. - throttle_manual_multiplier = 0.7 # keyboard signal is always 1 - brake_manual_multiplier = 0.7 # keyboard signal is always 1 - steer_manual_multiplier = 45 * STEER_RATIO # keyboard signal is always 1 - - # Simulation tends to be slow in the initial steps. This prevents lagging later - for _ in range(20): - world.tick() - - # loop - rk = Ratekeeper(100, print_delay_threshold=0.05) - - while self._keep_alive: - # 1. Read the throttle, steer and brake from op or manual controls - # 2. Set instructions in Carla - # 3. Send current carstate to op via can - - cruise_button = 0 - throttle_out = steer_out = brake_out = 0.0 - throttle_op = steer_op = brake_op = 0.0 - throttle_manual = steer_manual = brake_manual = 0.0 - - # --------------Step 1------------------------------- - if not q.empty(): - message = q.get() - m = message.split('_') - if m[0] == "steer": - steer_manual = float(m[1]) - is_openpilot_engaged = False - elif m[0] == "throttle": - throttle_manual = float(m[1]) - is_openpilot_engaged = False - elif m[0] == "brake": - brake_manual = float(m[1]) - is_openpilot_engaged = False - elif m[0] == "reverse": - cruise_button = CruiseButtons.CANCEL - is_openpilot_engaged = False - elif m[0] == "cruise": - if m[1] == "down": - cruise_button = CruiseButtons.DECEL_SET - is_openpilot_engaged = True - elif m[1] == "up": - cruise_button = CruiseButtons.RES_ACCEL - is_openpilot_engaged = True - elif m[1] == "cancel": - cruise_button = CruiseButtons.CANCEL - is_openpilot_engaged = False - elif m[0] == "ignition": - vehicle_state.ignition = not vehicle_state.ignition - elif m[0] == "quit": - break - - throttle_out = throttle_manual * throttle_manual_multiplier - steer_out = steer_manual * steer_manual_multiplier - brake_out = brake_manual * brake_manual_multiplier - - old_steer = steer_out - old_throttle = throttle_out - old_brake = brake_out - - if is_openpilot_engaged: - sm.update(0) - - # TODO gas and brake is deprecated - throttle_op = clip(sm['carControl'].actuators.accel / 1.6, 0.0, 1.0) - brake_op = clip(-sm['carControl'].actuators.accel / 4.0, 0.0, 1.0) - steer_op = sm['carControl'].actuators.steeringAngleDeg - - throttle_out = throttle_op - steer_out = steer_op - brake_out = brake_op - - steer_out = steer_rate_limit(old_steer, steer_out) - old_steer = steer_out - - else: - if throttle_out == 0 and old_throttle > 0: - if throttle_ease_out_counter > 0: - throttle_out = old_throttle - throttle_ease_out_counter += -1 - else: - throttle_ease_out_counter = REPEAT_COUNTER - old_throttle = 0 - - if brake_out == 0 and old_brake > 0: - if brake_ease_out_counter > 0: - brake_out = old_brake - brake_ease_out_counter += -1 - else: - brake_ease_out_counter = REPEAT_COUNTER - old_brake = 0 - - if steer_out == 0 and old_steer != 0: - if steer_ease_out_counter > 0: - steer_out = old_steer - steer_ease_out_counter += -1 - else: - steer_ease_out_counter = REPEAT_COUNTER - old_steer = 0 - - # --------------Step 2------------------------------- - steer_carla = steer_out / (max_steer_angle * STEER_RATIO * -1) - - steer_carla = np.clip(steer_carla, -1, 1) - steer_out = steer_carla * (max_steer_angle * STEER_RATIO * -1) - old_steer = steer_carla * (max_steer_angle * STEER_RATIO * -1) - - vc.throttle = throttle_out / 0.6 - vc.steer = steer_carla - vc.brake = brake_out - vehicle.apply_control(vc) - - # --------------Step 3------------------------------- - vel = vehicle.get_velocity() - speed = math.sqrt(vel.x ** 2 + vel.y ** 2 + vel.z ** 2) # in m/s - vehicle_state.speed = speed - vehicle_state.vel = vel - vehicle_state.angle = steer_out - vehicle_state.cruise_button = cruise_button - vehicle_state.is_engaged = is_openpilot_engaged - - if rk.frame % PRINT_DECIMATION == 0: - print("frame: ", "engaged:", is_openpilot_engaged, "; throttle: ", round(vc.throttle, 3), "; steer(c/deg): ", - round(vc.steer, 3), round(steer_out, 3), "; brake: ", round(vc.brake, 3)) - - if rk.frame % 5 == 0: - world.tick() - rk.keep_time() - self.started = True - - def close(self): - self.started = False - self._exit_event.set() - - for s in self._carla_objects: - try: - s.destroy() - except Exception as e: - print("Failed to destroy carla object", e) - for t in reversed(self._threads): - t.join() - - def run(self, queue, retries=-1): - bridge_p = Process(target=self.bridge_keep_alive, args=(queue, retries), daemon=True) - bridge_p.start() - return bridge_p - - -if __name__ == "__main__": - q: Any = Queue() - args = parse_args() - - carla_bridge = CarlaBridge(args) - p = carla_bridge.run(q) - - if args.joystick: - # start input poll for joystick - from openpilot.tools.sim.lib.manual_ctrl import wheel_poll_thread - - wheel_poll_thread(q) - else: - # start input poll for keyboard - from openpilot.tools.sim.lib.keyboard_ctrl import keyboard_poll_thread - - keyboard_poll_thread(q) - p.join() diff --git a/tools/sim/bridge/__init__.py b/tools/sim/bridge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/sim/bridge/carla/carla_bridge.py b/tools/sim/bridge/carla/carla_bridge.py new file mode 100644 index 0000000000..dc2377df30 --- /dev/null +++ b/tools/sim/bridge/carla/carla_bridge.py @@ -0,0 +1,22 @@ +from openpilot.tools.sim.bridge.common import SimulatorBridge +from openpilot.tools.sim.bridge.carla.carla_world import CarlaWorld + + +class CarlaBridge(SimulatorBridge): + TICKS_PER_FRAME = 5 + + def __init__(self, arguments): + super().__init__(arguments) + self.host = arguments.host + self.port = arguments.port + self.town = arguments.town + self.num_selected_spawn_point = arguments.num_selected_spawn_point + + def spawn_world(self): + import carla + + client = carla.Client(self.host, self.port) + client.set_timeout(5) + + return CarlaWorld(client, high_quality=self.high_quality, dual_camera=self.dual_camera, + num_selected_spawn_point=self.num_selected_spawn_point, town=self.town) \ No newline at end of file diff --git a/tools/sim/bridge/carla/carla_world.py b/tools/sim/bridge/carla/carla_world.py new file mode 100644 index 0000000000..14b300ed7b --- /dev/null +++ b/tools/sim/bridge/carla/carla_world.py @@ -0,0 +1,145 @@ +import numpy as np + +from openpilot.common.params import Params +from openpilot.tools.sim.lib.common import SimulatorState, vec3 +from openpilot.tools.sim.bridge.common import World +from openpilot.tools.sim.lib.camerad import W, H + + +class CarlaWorld(World): + def __init__(self, client, high_quality, dual_camera, num_selected_spawn_point, town): + super().__init__(dual_camera) + import carla + + low_quality_layers = carla.MapLayer(carla.MapLayer.Ground | carla.MapLayer.Walls | carla.MapLayer.Decals) + + layers = carla.MapLayer.All if high_quality else low_quality_layers + + world = client.load_world(town, map_layers=layers) + + settings = world.get_settings() + settings.fixed_delta_seconds = 0.01 + world.apply_settings(settings) + + world.set_weather(carla.WeatherParameters.ClearSunset) + + self.world = world + world_map = world.get_map() + + blueprint_library = world.get_blueprint_library() + + vehicle_bp = blueprint_library.filter('vehicle.tesla.*')[1] + vehicle_bp.set_attribute('role_name', 'hero') + spawn_points = world_map.get_spawn_points() + assert len(spawn_points) > num_selected_spawn_point, \ + f'''No spawn point {num_selected_spawn_point}, try a value between 0 and {len(spawn_points)} for this town.''' + self.spawn_point = spawn_points[num_selected_spawn_point] + self.vehicle = world.spawn_actor(vehicle_bp, self.spawn_point) + + physics_control = self.vehicle.get_physics_control() + physics_control.mass = 2326 + physics_control.torque_curve = [[20.0, 500.0], [5000.0, 500.0]] + physics_control.gear_switch_time = 0.0 + self.vehicle.apply_physics_control(physics_control) + + self.vc: carla.VehicleControl = carla.VehicleControl(throttle=0, steer=0, brake=0, reverse=False) + self.max_steer_angle: float = self.vehicle.get_physics_control().wheels[0].max_steer_angle + self.params = Params() + + self.steer_ratio = 15 + + self.carla_objects = [] + + transform = carla.Transform(carla.Location(x=0.8, z=1.13)) + + def create_camera(fov, callback): + blueprint = blueprint_library.find('sensor.camera.rgb') + blueprint.set_attribute('image_size_x', str(W)) + blueprint.set_attribute('image_size_y', str(H)) + blueprint.set_attribute('fov', str(fov)) + blueprint.set_attribute('sensor_tick', str(1/20)) + if not high_quality: + blueprint.set_attribute('enable_postprocess_effects', 'False') + camera = world.spawn_actor(blueprint, transform, attach_to=self.vehicle) + camera.listen(callback) + return camera + + self.road_camera = create_camera(fov=40, callback=self.cam_callback_road) + if dual_camera: + self.road_wide_camera = create_camera(fov=120, callback=self.cam_callback_wide_road) # fov bigger than 120 shows unwanted artifacts + else: + self.road_wide_camera = None + + # re-enable IMU + imu_bp = blueprint_library.find('sensor.other.imu') + imu_bp.set_attribute('sensor_tick', '0.01') + self.imu = world.spawn_actor(imu_bp, transform, attach_to=self.vehicle) + + gps_bp = blueprint_library.find('sensor.other.gnss') + self.gps = world.spawn_actor(gps_bp, transform, attach_to=self.vehicle) + self.params.put_bool("UbloxAvailable", True) + + self.carla_objects = [self.imu, self.gps, self.road_camera, self.road_wide_camera, self.vehicle] + + def close(self): + for s in self.carla_objects: + if s is not None: + try: + s.destroy() + except Exception as e: + print("Failed to destroy carla object", e) + + def carla_image_to_rgb(self, image): + rgb = np.frombuffer(image.raw_data, dtype=np.dtype("uint8")) + rgb = np.reshape(rgb, (H, W, 4)) + return np.ascontiguousarray(rgb[:, :, [0, 1, 2]]) + + def cam_callback_road(self, image): + with self.image_lock: + self.road_image = self.carla_image_to_rgb(image) + + def cam_callback_wide_road(self, image): + with self.image_lock: + self.wide_road_image = self.carla_image_to_rgb(image) + + def apply_controls(self, steer_angle, throttle_out, brake_out): + self.vc.throttle = throttle_out + + steer_carla = steer_angle * -1 / (self.max_steer_angle * self.steer_ratio) + steer_carla = np.clip(steer_carla, -1, 1) + + self.vc.steer = steer_carla + self.vc.brake = brake_out + self.vehicle.apply_control(self.vc) + + def read_sensors(self, simulator_state: SimulatorState): + simulator_state.imu.bearing = self.imu.get_transform().rotation.yaw + + simulator_state.imu.accelerometer = vec3( + self.imu.get_acceleration().x, + self.imu.get_acceleration().y, + self.imu.get_acceleration().z + ) + + simulator_state.imu.gyroscope = vec3( + self.imu.get_angular_velocity().x, + self.imu.get_angular_velocity().y, + self.imu.get_angular_velocity().z + ) + + simulator_state.gps.from_xy([self.vehicle.get_location().x, self.vehicle.get_location().y]) + + simulator_state.velocity = self.vehicle.get_velocity() + simulator_state.valid = True + simulator_state.steering_angle = self.vc.steer * self.max_steer_angle + + def read_cameras(self): + pass # cameras are read within a callback for carla + + def tick(self): + self.world.tick() + + def reset(self): + import carla + self.vehicle.set_transform(self.spawn_point) + self.vehicle.set_target_velocity(carla.Vector3D()) diff --git a/tools/sim/bridge/common.py b/tools/sim/bridge/common.py new file mode 100644 index 0000000000..a0e6538b2c --- /dev/null +++ b/tools/sim/bridge/common.py @@ -0,0 +1,186 @@ +import signal +import threading +import functools + +from multiprocessing import Process, Queue +from abc import ABC, abstractmethod +from typing import Optional + +from openpilot.common.params import Params +from openpilot.common.numpy_fast import clip +from openpilot.common.realtime import Ratekeeper +from openpilot.selfdrive.test.helpers import set_params_enabled +from openpilot.selfdrive.car.honda.values import CruiseButtons +from openpilot.tools.sim.lib.common import SimulatorState, World +from openpilot.tools.sim.lib.simulated_car import SimulatedCar +from openpilot.tools.sim.lib.simulated_sensors import SimulatedSensors +from openpilot.tools.sim.lib.keyboard_ctrl import KEYBOARD_HELP + + +def rk_loop(function, hz, exit_event: threading.Event): + rk = Ratekeeper(hz, None) + while not exit_event.is_set(): + function() + rk.keep_time() + + +class SimulatorBridge(ABC): + TICKS_PER_FRAME = 5 + + def __init__(self, arguments): + set_params_enabled() + self.params = Params() + + self.rk = Ratekeeper(100, None) + + self.dual_camera = arguments.dual_camera + self.high_quality = arguments.high_quality + + self._exit_event = threading.Event() + self._threads = [] + self._keep_alive = True + self.started = False + signal.signal(signal.SIGTERM, self._on_shutdown) + self._exit = threading.Event() + self.simulator_state = SimulatorState() + + self.world: Optional[World] = None + + self.past_startup_engaged = False + + def _on_shutdown(self, signal, frame): + self.shutdown() + + def shutdown(self): + self._keep_alive = False + + def bridge_keep_alive(self, q: Queue, retries: int): + try: + self._run(q) + finally: + self.close() + + def close(self): + self.started = False + self._exit_event.set() + + if self.world is not None: + self.world.close() + + def run(self, queue, retries=-1): + bridge_p = Process(name="bridge", target=self.bridge_keep_alive, args=(queue, retries)) + bridge_p.start() + return bridge_p + + def print_status(self): + print( + f""" +Keyboard Commands: +{KEYBOARD_HELP} + +State: +Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_engaged} + """) + + @abstractmethod + def spawn_world(self) -> World: + pass + + def _run(self, q: Queue): + self.world = self.spawn_world() + + self.simulated_car = SimulatedCar() + self.simulated_sensors = SimulatedSensors(self.dual_camera) + + self.simulated_car_thread = threading.Thread(target=rk_loop, args=(functools.partial(self.simulated_car.update, self.simulator_state), + 100, self._exit_event)) + self.simulated_car_thread.start() + + self.simulated_camera_thread = threading.Thread(target=rk_loop, args=(functools.partial(self.simulated_sensors.send_camera_images, self.world), + 20, self._exit_event)) + self.simulated_camera_thread.start() + + # Simulation tends to be slow in the initial steps. This prevents lagging later + for _ in range(20): + self.world.tick() + + while self._keep_alive: + throttle_out = steer_out = brake_out = 0.0 + throttle_op = steer_op = brake_op = 0.0 + + self.simulator_state.cruise_button = 0 + self.simulator_state.left_blinker = False + self.simulator_state.right_blinker = False + + throttle_manual = steer_manual = brake_manual = 0. + + # Read manual controls + if not q.empty(): + message = q.get() + m = message.split('_') + if m[0] == "steer": + steer_manual = float(m[1]) + elif m[0] == "throttle": + throttle_manual = float(m[1]) + elif m[0] == "brake": + brake_manual = float(m[1]) + elif m[0] == "cruise": + if m[1] == "down": + self.simulator_state.cruise_button = CruiseButtons.DECEL_SET + elif m[1] == "up": + self.simulator_state.cruise_button = CruiseButtons.RES_ACCEL + elif m[1] == "cancel": + self.simulator_state.cruise_button = CruiseButtons.CANCEL + elif m[1] == "main": + self.simulator_state.cruise_button = CruiseButtons.MAIN + elif m[0] == "blinker": + if m[1] == "left": + self.simulator_state.left_blinker = True + elif m[1] == "right": + self.simulator_state.right_blinker = True + elif m[0] == "ignition": + self.simulator_state.ignition = not self.simulator_state.ignition + elif m[0] == "reset": + self.world.reset() + elif m[0] == "quit": + break + + self.simulator_state.user_brake = brake_manual + self.simulator_state.user_gas = throttle_manual + self.simulator_state.user_torque = steer_manual * 10000 + + steer_manual = steer_manual * -40 + + # Update openpilot on current sensor state + self.simulated_sensors.update(self.simulator_state, self.world) + + self.simulated_car.sm.update(0) + controlsState = self.simulated_car.sm['controlsState'] + self.simulator_state.is_engaged = controlsState.active + + if self.simulator_state.is_engaged: + throttle_op = clip(self.simulated_car.sm['carControl'].actuators.accel / 1.6, 0.0, 1.0) + brake_op = clip(-self.simulated_car.sm['carControl'].actuators.accel / 4.0, 0.0, 1.0) + steer_op = self.simulated_car.sm['carControl'].actuators.steeringAngleDeg + + self.past_startup_engaged = True + elif not self.past_startup_engaged and controlsState.engageable: + self.simulator_state.cruise_button = CruiseButtons.DECEL_SET # force engagement on startup + + throttle_out = throttle_op if self.simulator_state.is_engaged else throttle_manual + brake_out = brake_op if self.simulator_state.is_engaged else brake_manual + steer_out = steer_op if self.simulator_state.is_engaged else steer_manual + + self.world.apply_controls(steer_out, throttle_out, brake_out) + self.world.read_sensors(self.simulator_state) + + if self.rk.frame % self.TICKS_PER_FRAME == 0: + self.world.tick() + self.world.read_cameras() + + if self.rk.frame % 25 == 0: + self.print_status() + + self.started = True + + self.rk.keep_time() diff --git a/tools/sim/bridge/metadrive/metadrive_bridge.py b/tools/sim/bridge/metadrive/metadrive_bridge.py new file mode 100644 index 0000000000..fa946e11e3 --- /dev/null +++ b/tools/sim/bridge/metadrive/metadrive_bridge.py @@ -0,0 +1,119 @@ +import numpy as np + +from metadrive.component.sensors.rgb_camera import RGBCamera +from metadrive.component.sensors.base_camera import _cuda_enable +from metadrive.component.map.pg_map import MapGenerateMethod +from panda3d.core import Vec3, Texture, GraphicsOutput + +from openpilot.tools.sim.bridge.common import SimulatorBridge +from openpilot.tools.sim.bridge.metadrive.metadrive_world import MetaDriveWorld +from openpilot.tools.sim.lib.camerad import W, H + + +C3_POSITION = Vec3(0, 0, 1) + + +class CopyRamRGBCamera(RGBCamera): + """Camera which copies its content into RAM during the render process, for faster image grabbing.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.cpu_texture = Texture() + self.buffer.addRenderTexture(self.cpu_texture, GraphicsOutput.RTMCopyRam) + + def get_rgb_array_cpu(self): + origin_img = self.cpu_texture + img = np.frombuffer(origin_img.getRamImage().getData(), dtype=np.uint8) + img = img.reshape((origin_img.getYSize(), origin_img.getXSize(), -1)) + img = img[:,:,:3] # RGBA to RGB + # img = np.swapaxes(img, 1, 0) + img = img[::-1] # Flip on vertical axis + return img + + +class RGBCameraWide(CopyRamRGBCamera): + def __init__(self, *args, **kwargs): + super(RGBCameraWide, self).__init__(*args, **kwargs) + cam = self.get_cam() + cam.setPos(C3_POSITION) + lens = self.get_lens() + lens.setFov(160) + +class RGBCameraRoad(CopyRamRGBCamera): + def __init__(self, *args, **kwargs): + super(RGBCameraRoad, self).__init__(*args, **kwargs) + cam = self.get_cam() + cam.setPos(C3_POSITION) + lens = self.get_lens() + lens.setFov(40) + + +def straight_block(length): + return { + "id": "S", + "pre_block_socket_index": 0, + "length": length + } + +def curve_block(length, angle=45, direction=0): + return { + "id": "C", + "pre_block_socket_index": 0, + "length": length, + "radius": length, + "angle": angle, + "dir": direction + } + + +class MetaDriveBridge(SimulatorBridge): + TICKS_PER_FRAME = 5 + + def __init__(self, args): + self.should_render = False + + super(MetaDriveBridge, self).__init__(args) + + def spawn_world(self): + sensors = { + "rgb_road": (RGBCameraRoad, W, H, ) + } + + if self.dual_camera: + sensors["rgb_wide"] = (RGBCameraWide, W, H) + + config = dict( + use_render=self.should_render, + vehicle_config=dict( + enable_reverse=False, + image_source="rgb_road", + spawn_longitude=15 + ), + sensors=sensors, + image_on_cuda=_cuda_enable, + image_observation=True, + interface_panel=[], + out_of_route_done=False, + on_continuous_line_done=False, + crash_vehicle_done=False, + crash_object_done=False, + traffic_density=0.0, # traffic is incredibly expensive + map_config=dict( + type=MapGenerateMethod.PG_MAP_FILE, + config=[ + None, + straight_block(120), + curve_block(240, 90), + straight_block(120), + curve_block(240, 90), + straight_block(120), + curve_block(240, 90), + straight_block(120), + curve_block(240, 90), + ] + ), + decision_repeat=1, + physics_world_step_size=self.TICKS_PER_FRAME/100, + preload_models=False + ) + + return MetaDriveWorld(config) diff --git a/tools/sim/bridge/metadrive/metadrive_process.py b/tools/sim/bridge/metadrive/metadrive_process.py new file mode 100644 index 0000000000..3d21c0e508 --- /dev/null +++ b/tools/sim/bridge/metadrive/metadrive_process.py @@ -0,0 +1,99 @@ +import math +import numpy as np + +from collections import namedtuple +from multiprocessing.connection import Connection + +from metadrive.engine.core.engine_core import EngineCore +from metadrive.engine.core.image_buffer import ImageBuffer +from metadrive.envs.metadrive_env import MetaDriveEnv +from metadrive.obs.image_obs import ImageObservation + +from openpilot.common.realtime import Ratekeeper +from openpilot.tools.sim.lib.common import vec3 +from openpilot.tools.sim.lib.camerad import W, H + + +metadrive_state = namedtuple("metadrive_state", ["velocity", "position", "bearing", "steering_angle"]) + +def apply_metadrive_patches(): + # By default, metadrive won't try to use cuda images unless it's used as a sensor for vehicles, so patch that in + def add_image_sensor_patched(self, name: str, cls, args): + if self.global_config["image_on_cuda"]:# and name == self.global_config["vehicle_config"]["image_source"]: + sensor = cls(*args, self, cuda=True) + else: + sensor = cls(*args, self, cuda=False) + assert isinstance(sensor, ImageBuffer), "This API is for adding image sensor" + self.sensors[name] = sensor + + EngineCore.add_image_sensor = add_image_sensor_patched + + # we aren't going to use the built-in observation stack, so disable it to save time + def observe_patched(self, vehicle): + return self.state + + ImageObservation.observe = observe_patched + + def arrive_destination_patch(self, vehicle): + return False + + MetaDriveEnv._is_arrive_destination = arrive_destination_patch + +def metadrive_process(dual_camera: bool, config: dict, camera_array, controls_recv: Connection, state_send: Connection, exit_event): + apply_metadrive_patches() + + road_image = np.frombuffer(camera_array.get_obj(), dtype=np.uint8).reshape((H, W, 3)) + + env = MetaDriveEnv(config) + + def reset(): + env.reset() + env.vehicle.config["max_speed_km_h"] = 1000 + + reset() + + def get_cam_as_rgb(cam): + cam = env.engine.sensors[cam] + img = cam.perceive(env.vehicle, clip=False) + if type(img) != np.ndarray: + img = img.get() # convert cupy array to numpy + return img + + rk = Ratekeeper(100, None) + + steer_ratio = 15 + vc = [0,0] + + while not exit_event.is_set(): + state = metadrive_state( + velocity=vec3(x=float(env.vehicle.velocity[0]), y=float(env.vehicle.velocity[1]), z=0), + position=env.vehicle.position, + bearing=float(math.degrees(env.vehicle.heading_theta)), + steering_angle=env.vehicle.steering * env.vehicle.MAX_STEERING + ) + + state_send.send(state) + + if controls_recv.poll(0): + while controls_recv.poll(0): + steer_angle, gas, should_reset = controls_recv.recv() + + steer_metadrive = steer_angle * 1 / (env.vehicle.MAX_STEERING * steer_ratio) + steer_metadrive = np.clip(steer_metadrive, -1, 1) + + vc = [steer_metadrive, gas] + + if should_reset: + reset() + + if rk.frame % 5 == 0: + obs, _, terminated, _, info = env.step(vc) + + if terminated: + reset() + + #if dual_camera: + # wide_road_image = get_cam_as_rgb("rgb_wide") + road_image[...] = get_cam_as_rgb("rgb_road") + + rk.keep_time() \ No newline at end of file diff --git a/tools/sim/bridge/metadrive/metadrive_world.py b/tools/sim/bridge/metadrive/metadrive_world.py new file mode 100644 index 0000000000..3705f19a97 --- /dev/null +++ b/tools/sim/bridge/metadrive/metadrive_world.py @@ -0,0 +1,75 @@ +import ctypes +import functools +import multiprocessing +import numpy as np +import time + +from multiprocessing import Pipe, Array +from openpilot.tools.sim.bridge.metadrive.metadrive_process import metadrive_process, metadrive_state +from openpilot.tools.sim.lib.common import SimulatorState, World +from openpilot.tools.sim.lib.camerad import W, H + + +class MetaDriveWorld(World): + def __init__(self, config, dual_camera = False): + super().__init__(dual_camera) + self.camera_array = Array(ctypes.c_uint8, W*H*3) + self.road_image = np.frombuffer(self.camera_array.get_obj(), dtype=np.uint8).reshape((H, W, 3)) + + self.controls_send, self.controls_recv = Pipe() + self.state_send, self.state_recv = Pipe() + + self.exit_event = multiprocessing.Event() + + self.metadrive_process = multiprocessing.Process(name="metadrive process", target= + functools.partial(metadrive_process, dual_camera, config, + self.camera_array, self.controls_recv, self.state_send, self.exit_event)) + self.metadrive_process.start() + + print("----------------------------------------------------------") + print("---- Spawning Metadrive world, this might take awhile ----") + print("----------------------------------------------------------") + + self.state_recv.recv() # wait for a state message to ensure metadrive is launched + + self.steer_ratio = 15 + self.vc = [0.0,0.0] + self.reset_time = 0 + self.should_reset = False + + def apply_controls(self, steer_angle, throttle_out, brake_out): + if (time.monotonic() - self.reset_time) > 2: + self.vc[0] = steer_angle + + if throttle_out: + self.vc[1] = throttle_out + else: + self.vc[1] = -brake_out + else: + self.vc[0] = 0 + self.vc[1] = 0 + + self.controls_send.send([*self.vc, self.should_reset]) + self.should_reset = False + + def read_sensors(self, state: SimulatorState): + while self.state_recv.poll(0): + md_state: metadrive_state = self.state_recv.recv() + state.velocity = md_state.velocity + state.bearing = md_state.bearing + state.steering_angle = md_state.steering_angle + state.gps.from_xy(md_state.position) + state.valid = True + + def read_cameras(self): + pass + + def tick(self): + pass + + def reset(self): + self.should_reset = True + + def close(self): + self.exit_event.set() + self.metadrive_process.join() diff --git a/tools/sim/launch_openpilot.sh b/tools/sim/launch_openpilot.sh index d5922a4819..9532537283 100755 --- a/tools/sim/launch_openpilot.sh +++ b/tools/sim/launch_openpilot.sh @@ -6,11 +6,16 @@ export SIMULATION="1" export SKIP_FW_QUERY="1" export FINGERPRINT="HONDA CIVIC 2016" -export BLOCK="camerad,loggerd,encoderd,micd,logmessaged" +export BLOCK="${BLOCK},camerad,loggerd,encoderd,micd,logmessaged" if [[ "$CI" ]]; then # TODO: offscreen UI should work export BLOCK="${BLOCK},ui" fi +python -c "from openpilot.selfdrive.test.helpers import set_params_enabled; set_params_enabled()" + +SCRIPT_DIR=$(dirname "$0") +OPENPILOT_DIR=$SCRIPT_DIR/../../ + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd ../../selfdrive/manager && exec ./manager.py +cd $OPENPILOT_DIR/selfdrive/manager && exec ./manager.py diff --git a/tools/sim/lib/camerad.py b/tools/sim/lib/camerad.py new file mode 100644 index 0000000000..4621347787 --- /dev/null +++ b/tools/sim/lib/camerad.py @@ -0,0 +1,70 @@ +import numpy as np +import os +import pyopencl as cl +import pyopencl.array as cl_array + +from cereal.visionipc import VisionIpcServer, VisionStreamType +from cereal import messaging + +from openpilot.common.basedir import BASEDIR +from openpilot.tools.sim.lib.common import W, H + +class Camerad: + """Simulates the camerad daemon""" + def __init__(self, dual_camera): + self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState']) + + self.frame_road_id = 0 + self.frame_wide_id = 0 + self.vipc_server = VisionIpcServer("camerad") + + self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, False, W, H) + if dual_camera: + self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 5, False, W, H) + + self.vipc_server.start_listener() + + # set up for pyopencl rgb to yuv conversion + self.ctx = cl.create_some_context() + self.queue = cl.CommandQueue(self.ctx) + cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG " + + kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl") + with open(kernel_fn) as f: + prg = cl.Program(self.ctx, f.read()).build(cl_arg) + self.krnl = prg.rgb_to_nv12 + self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4 + self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4 + + def cam_send_yuv_road(self, yuv): + self._send_yuv(yuv, self.frame_road_id, 'roadCameraState', VisionStreamType.VISION_STREAM_ROAD) + self.frame_road_id += 1 + + def cam_send_yuv_wide_road(self, yuv): + self._send_yuv(yuv, self.frame_wide_id, 'wideRoadCameraState', VisionStreamType.VISION_STREAM_WIDE_ROAD) + self.frame_wide_id += 1 + + # Returns: yuv bytes + def rgb_to_yuv(self, rgb): + assert rgb.shape == (H, W, 3), f"{rgb.shape}" + assert rgb.dtype == np.uint8 + + rgb_cl = cl_array.to_device(self.queue, rgb) + yuv_cl = cl_array.empty_like(rgb_cl) + self.krnl(self.queue, (self.Wdiv4, self.Hdiv4), None, rgb_cl.data, yuv_cl.data).wait() + yuv = np.resize(yuv_cl.get(), rgb.size // 2) + return yuv.data.tobytes() + + def _send_yuv(self, yuv, frame_id, pub_type, yuv_type): + eof = int(frame_id * 0.05 * 1e9) + self.vipc_server.send(yuv_type, yuv, frame_id, eof, eof) + + dat = messaging.new_message(pub_type) + msg = { + "frameId": frame_id, + "transform": [1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0] + } + setattr(dat, pub_type, msg) + self.pm.send(pub_type, dat) \ No newline at end of file diff --git a/tools/sim/lib/can.py b/tools/sim/lib/can.py deleted file mode 100755 index db321d3193..0000000000 --- a/tools/sim/lib/can.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -import cereal.messaging as messaging -from opendbc.can.packer import CANPacker -from opendbc.can.parser import CANParser -from openpilot.selfdrive.boardd.boardd_api_impl import can_list_to_can_capnp -from openpilot.selfdrive.car import crc8_pedal - -packer = CANPacker("honda_civic_touring_2016_can_generated") -rpacker = CANPacker("acura_ilx_2016_nidec") - - -def get_car_can_parser(): - dbc_f = 'honda_civic_touring_2016_can_generated' - checks = [ - (0xe4, 100), - (0x1fa, 50), - (0x200, 50), - ] - return CANParser(dbc_f, checks, 0) -cp = get_car_can_parser() - -def can_function(pm, speed, angle, idx, cruise_button, is_engaged): - - msg = [] - - # *** powertrain bus *** - - speed = speed * 3.6 # convert m/s to kph - msg.append(packer.make_can_msg("ENGINE_DATA", 0, {"XMISSION_SPEED": speed})) - msg.append(packer.make_can_msg("WHEEL_SPEEDS", 0, { - "WHEEL_SPEED_FL": speed, - "WHEEL_SPEED_FR": speed, - "WHEEL_SPEED_RL": speed, - "WHEEL_SPEED_RR": speed - })) - - msg.append(packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": cruise_button})) - - values = {"COUNTER_PEDAL": idx & 0xF} - checksum = crc8_pedal(packer.make_can_msg("GAS_SENSOR", 0, {"COUNTER_PEDAL": idx & 0xF})[2][:-1]) - values["CHECKSUM_PEDAL"] = checksum - msg.append(packer.make_can_msg("GAS_SENSOR", 0, values)) - - msg.append(packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8})) - msg.append(packer.make_can_msg("GAS_PEDAL_2", 0, {})) - msg.append(packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1})) - msg.append(packer.make_can_msg("STEER_STATUS", 0, {})) - msg.append(packer.make_can_msg("STEERING_SENSORS", 0, {"STEER_ANGLE": angle})) - msg.append(packer.make_can_msg("VSA_STATUS", 0, {})) - msg.append(packer.make_can_msg("STANDSTILL", 0, {"WHEELS_MOVING": 1 if speed >= 1.0 else 0})) - msg.append(packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {})) - msg.append(packer.make_can_msg("EPB_STATUS", 0, {})) - msg.append(packer.make_can_msg("DOORS_STATUS", 0, {})) - msg.append(packer.make_can_msg("CRUISE_PARAMS", 0, {})) - msg.append(packer.make_can_msg("CRUISE", 0, {})) - msg.append(packer.make_can_msg("SCM_FEEDBACK", 0, {"MAIN_ON": 1})) - msg.append(packer.make_can_msg("POWERTRAIN_DATA", 0, {"ACC_STATUS": int(is_engaged)})) - msg.append(packer.make_can_msg("HUD_SETTING", 0, {})) - msg.append(packer.make_can_msg("CAR_SPEED", 0, {})) - - # *** cam bus *** - msg.append(packer.make_can_msg("STEERING_CONTROL", 2, {})) - msg.append(packer.make_can_msg("ACC_HUD", 2, {})) - msg.append(packer.make_can_msg("LKAS_HUD", 2, {})) - msg.append(packer.make_can_msg("BRAKE_COMMAND", 2, {})) - - # *** radar bus *** - if idx % 5 == 0: - msg.append(rpacker.make_can_msg("RADAR_DIAGNOSTIC", 1, {"RADAR_STATE": 0x79})) - for i in range(16): - msg.append(rpacker.make_can_msg("TRACK_%d" % i, 1, {"LONG_DIST": 255.5})) - - pm.send('can', can_list_to_can_capnp(msg)) - -def sendcan_function(sendcan): - sc = messaging.drain_sock_raw(sendcan) - cp.update_strings(sc, sendcan=True) - - if cp.vl[0x1fa]['COMPUTER_BRAKE_REQUEST']: - brake = cp.vl[0x1fa]['COMPUTER_BRAKE'] / 1024. - else: - brake = 0.0 - - if cp.vl[0x200]['GAS_COMMAND'] > 0: - gas = ( cp.vl[0x200]['GAS_COMMAND'] + 83.3 ) / (0.253984064 * 2**16) - else: - gas = 0.0 - - if cp.vl[0xe4]['STEER_TORQUE_REQUEST']: - steer_torque = cp.vl[0xe4]['STEER_TORQUE']/3840 - else: - steer_torque = 0.0 - - return gas, brake, steer_torque diff --git a/tools/sim/lib/common.py b/tools/sim/lib/common.py new file mode 100644 index 0000000000..b7aa66f5c5 --- /dev/null +++ b/tools/sim/lib/common.py @@ -0,0 +1,94 @@ +import math +import threading +import numpy as np + +from abc import ABC, abstractmethod +from collections import namedtuple + +W, H = 1928, 1208 + + +vec3 = namedtuple("vec3", ["x", "y", "z"]) + +class GPSState: + def __init__(self): + self.latitude = 0 + self.longitude = 0 + self.altitude = 0 + + def from_xy(self, xy): + """Simulates a lat/lon from an xy coordinate on a plane, for simple simlation. TODO: proper global projection?""" + BASE_LAT = 32.75308505188913 + BASE_LON = -117.2095393365393 + DEG_TO_METERS = 100000 + + self.latitude = float(BASE_LAT + xy[0] / DEG_TO_METERS) + self.longitude = float(BASE_LON + xy[1] / DEG_TO_METERS) + self.altitude = 0 + + +class IMUState: + def __init__(self): + self.accelerometer: vec3 = vec3(0,0,0) + self.gyroscope: vec3 = vec3(0,0,0) + self.bearing: float = 0 + + +class SimulatorState: + def __init__(self): + self.valid = False + self.is_engaged = False + self.ignition = True + + self.velocity: vec3 = None + self.bearing: float = 0 + self.gps = GPSState() + self.imu = IMUState() + + self.steering_angle: float = 0 + + self.user_gas: float = 0 + self.user_brake: float = 0 + self.user_torque: float = 0 + + self.cruise_button = 0 + + self.left_blinker = False + self.right_blinker = False + + @property + def speed(self): + return math.sqrt(self.velocity.x ** 2 + self.velocity.y ** 2 + self.velocity.z ** 2) + + +class World(ABC): + def __init__(self, dual_camera): + self.dual_camera = dual_camera + + self.image_lock = threading.Lock() + self.road_image = np.zeros((H, W, 3), dtype=np.uint8) + self.wide_road_image = np.zeros((H, W, 3), dtype=np.uint8) + + @abstractmethod + def apply_controls(self, steer_sim, throttle_out, brake_out): + pass + + @abstractmethod + def tick(self): + pass + + @abstractmethod + def read_sensors(self, simulator_state: SimulatorState): + pass + + @abstractmethod + def read_cameras(self): + pass + + @abstractmethod + def close(self): + pass + + @abstractmethod + def reset(self): + pass \ No newline at end of file diff --git a/tools/sim/lib/keyboard_ctrl.py b/tools/sim/lib/keyboard_ctrl.py index 803aa091a3..c37f6ea479 100644 --- a/tools/sim/lib/keyboard_ctrl.py +++ b/tools/sim/lib/keyboard_ctrl.py @@ -1,6 +1,7 @@ import sys import termios import time + from termios import (BRKINT, CS8, CSIZE, ECHO, ICANON, ICRNL, IEXTEN, INPCK, ISTRIP, IXON, PARENB, VMIN, VTIME) from typing import NoReturn @@ -16,6 +17,20 @@ CC = 6 STDIN_FD = sys.stdin.fileno() + +KEYBOARD_HELP = """ + | key | functionality | + |------|-----------------------| + | 1 | Cruise Resume / Accel | + | 2 | Cruise Set / Decel | + | 3 | Cruise Cancel | + | r | Reset Simulation | + | i | Toggle Ignition | + | q | Exit all | + | wasd | Control manually | +""" + + def getch() -> str: old_settings = termios.tcgetattr(STDIN_FD) try: @@ -38,7 +53,6 @@ def getch() -> str: def keyboard_poll_thread(q: 'Queue[str]'): while True: c = getch() - print("got %s" % c) if c == '1': q.put("cruise_up") elif c == '2': @@ -53,8 +67,14 @@ def keyboard_poll_thread(q: 'Queue[str]'): q.put("brake_%f" % 1.0) elif c == 'd': q.put("steer_%f" % -0.15) + elif c == 'z': + q.put("blinker_left") + elif c == 'x': + q.put("blinker_right") elif c == 'i': q.put("ignition") + elif c == 'r': + q.put("reset") elif c == 'q': q.put("quit") break diff --git a/tools/sim/lib/manual_ctrl.py b/tools/sim/lib/manual_ctrl.py index 1687a2e6ba..04e228c18c 100755 --- a/tools/sim/lib/manual_ctrl.py +++ b/tools/sim/lib/manual_ctrl.py @@ -4,7 +4,7 @@ import array import os import struct from fcntl import ioctl -from typing import NoReturn +from typing import NoReturn, Dict, List # Iterate over the joystick devices. print('Available devices:') @@ -13,8 +13,8 @@ for fn in os.listdir('/dev/input'): print(f' /dev/input/{fn}') # We'll store the states here. -axis_states = {} -button_states = {} +axis_states: Dict[str, float] = {} +button_states: Dict[str, float] = {} # These constants were borrowed from linux/input.h axis_names = { @@ -88,8 +88,8 @@ button_names = { 0x2c3 : 'dpad_down', } -axis_map = [] -button_map = [] +axis_name_list: List[str] = [] +button_name_list: List[str] = [] def wheel_poll_thread(q: 'Queue[str]') -> NoReturn: # Open the joystick device. @@ -119,7 +119,7 @@ def wheel_poll_thread(q: 'Queue[str]') -> NoReturn: for _axis in buf[:num_axes]: axis_name = axis_names.get(_axis, f'unknown(0x{_axis:02x})') - axis_map.append(axis_name) + axis_name_list.append(axis_name) axis_states[axis_name] = 0.0 # Get the button map. @@ -128,11 +128,11 @@ def wheel_poll_thread(q: 'Queue[str]') -> NoReturn: for btn in buf[:num_buttons]: btn_name = button_names.get(btn, f'unknown(0x{btn:03x})') - button_map.append(btn_name) + button_name_list.append(btn_name) button_states[btn_name] = 0 - print('%d axes found: %s' % (num_axes, ', '.join(axis_map))) - print('%d buttons found: %s' % (num_buttons, ', '.join(button_map))) + print('%d axes found: %s' % (num_axes, ', '.join(axis_name_list))) + print('%d buttons found: %s' % (num_buttons, ', '.join(button_name_list))) # Enable FF import evdev @@ -147,7 +147,7 @@ def wheel_poll_thread(q: 'Queue[str]') -> NoReturn: value, mtype, number = struct.unpack('4xhBB', evbuf) # print(mtype, number, value) if mtype & 0x02: # wheel & paddles - axis = axis_map[number] + axis = axis_name_list[number] if axis == "z": # gas fvalue = value / 32767.0 diff --git a/tools/sim/lib/simulated_car.py b/tools/sim/lib/simulated_car.py new file mode 100644 index 0000000000..ab60fad8d3 --- /dev/null +++ b/tools/sim/lib/simulated_car.py @@ -0,0 +1,119 @@ +import cereal.messaging as messaging + +from opendbc.can.packer import CANPacker +from opendbc.can.parser import CANParser +from openpilot.selfdrive.boardd.boardd_api_impl import can_list_to_can_capnp +from openpilot.selfdrive.car import crc8_pedal +from openpilot.tools.sim.lib.common import SimulatorState + + +class SimulatedCar: + """Simulates a honda civic 2016 (panda state + can messages) to OpenPilot""" + packer = CANPacker("honda_civic_touring_2016_can_generated") + rpacker = CANPacker("acura_ilx_2016_nidec") + + def __init__(self): + self.pm = messaging.PubMaster(['can', 'pandaStates']) + self.sm = messaging.SubMaster(['carControl', 'controlsState', 'carParams']) + self.cp = self.get_car_can_parser() + self.idx = 0 + + @staticmethod + def get_car_can_parser(): + dbc_f = 'honda_civic_touring_2016_can_generated' + checks = [ + (0xe4, 100), + (0x1fa, 50), + (0x200, 50), + ] + return CANParser(dbc_f, checks, 0) + + def send_can_messages(self, simulator_state: SimulatorState): + if not simulator_state.valid: + return + + msg = [] + + # *** powertrain bus *** + + speed = simulator_state.speed * 3.6 # convert m/s to kph + msg.append(self.packer.make_can_msg("ENGINE_DATA", 0, {"XMISSION_SPEED": speed})) + msg.append(self.packer.make_can_msg("WHEEL_SPEEDS", 0, { + "WHEEL_SPEED_FL": speed, + "WHEEL_SPEED_FR": speed, + "WHEEL_SPEED_RL": speed, + "WHEEL_SPEED_RR": speed + })) + + msg.append(self.packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": simulator_state.cruise_button})) + + values = { + "COUNTER_PEDAL": self.idx & 0xF, + "INTERCEPTOR_GAS": simulator_state.user_gas * 2**12, + "INTERCEPTOR_GAS2": simulator_state.user_gas * 2**12, + } + checksum = crc8_pedal(self.packer.make_can_msg("GAS_SENSOR", 0, values)[2][:-1]) + values["CHECKSUM_PEDAL"] = checksum + msg.append(self.packer.make_can_msg("GAS_SENSOR", 0, values)) + + msg.append(self.packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8})) + msg.append(self.packer.make_can_msg("GAS_PEDAL_2", 0, {})) + msg.append(self.packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1})) + msg.append(self.packer.make_can_msg("STEER_STATUS", 0, {"STEER_TORQUE_SENSOR": simulator_state.user_torque})) + msg.append(self.packer.make_can_msg("STEERING_SENSORS", 0, {"STEER_ANGLE": simulator_state.steering_angle})) + msg.append(self.packer.make_can_msg("VSA_STATUS", 0, {})) + msg.append(self.packer.make_can_msg("STANDSTILL", 0, {"WHEELS_MOVING": 1 if simulator_state.speed >= 1.0 else 0})) + msg.append(self.packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {})) + msg.append(self.packer.make_can_msg("EPB_STATUS", 0, {})) + msg.append(self.packer.make_can_msg("DOORS_STATUS", 0, {})) + msg.append(self.packer.make_can_msg("CRUISE_PARAMS", 0, {})) + msg.append(self.packer.make_can_msg("CRUISE", 0, {})) + msg.append(self.packer.make_can_msg("SCM_FEEDBACK", 0, + { + "MAIN_ON": 1, + "LEFT_BLINKER": simulator_state.left_blinker, + "RIGHT_BLINKER": simulator_state.right_blinker + })) + msg.append(self.packer.make_can_msg("POWERTRAIN_DATA", 0, + { + "ACC_STATUS": int(simulator_state.is_engaged), + "PEDAL_GAS": simulator_state.user_gas, + "BRAKE_PRESSED": simulator_state.user_brake > 0 + })) + msg.append(self.packer.make_can_msg("HUD_SETTING", 0, {})) + msg.append(self.packer.make_can_msg("CAR_SPEED", 0, {})) + + # *** cam bus *** + msg.append(self.packer.make_can_msg("STEERING_CONTROL", 2, {})) + msg.append(self.packer.make_can_msg("ACC_HUD", 2, {})) + msg.append(self.packer.make_can_msg("LKAS_HUD", 2, {})) + msg.append(self.packer.make_can_msg("BRAKE_COMMAND", 2, {})) + + # *** radar bus *** + if self.idx % 5 == 0: + msg.append(self.rpacker.make_can_msg("RADAR_DIAGNOSTIC", 1, {"RADAR_STATE": 0x79})) + for i in range(16): + msg.append(self.rpacker.make_can_msg("TRACK_%d" % i, 1, {"LONG_DIST": 255.5})) + + self.pm.send('can', can_list_to_can_capnp(msg)) + + def send_panda_state(self, simulator_state): + self.sm.update(0) + dat = messaging.new_message('pandaStates', 1) + dat.valid = True + dat.pandaStates[0] = { + 'ignitionLine': simulator_state.ignition, + 'pandaType': "blackPanda", + 'controlsAllowed': True, + 'safetyModel': 'hondaNidec', + 'alternativeExperience': self.sm["carParams"].alternativeExperience + } + self.pm.send('pandaStates', dat) + + def update(self, simulator_state: SimulatorState): + self.send_can_messages(simulator_state) + + if self.idx % 50 == 0: # only send panda states at 2hz + self.send_panda_state(simulator_state) + + self.idx += 1 \ No newline at end of file diff --git a/tools/sim/lib/simulated_sensors.py b/tools/sim/lib/simulated_sensors.py new file mode 100644 index 0000000000..4520ed35ae --- /dev/null +++ b/tools/sim/lib/simulated_sensors.py @@ -0,0 +1,125 @@ +import time + +from cereal import log +import cereal.messaging as messaging + +from openpilot.common.params import Params +from openpilot.common.realtime import DT_DMON +from openpilot.tools.sim.lib.camerad import Camerad + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from openpilot.tools.sim.lib.common import World, SimulatorState + + +class SimulatedSensors: + """Simulates the C3 sensors (acc, gyro, gps, peripherals, dm state, cameras) to OpenPilot""" + + def __init__(self, dual_camera=False): + self.pm = messaging.PubMaster(['accelerometer', 'gyroscope', 'gpsLocationExternal', 'driverStateV2', 'driverMonitoringState', 'peripheralState']) + self.camerad = Camerad(dual_camera=dual_camera) + self.last_perp_update = 0 + self.last_dmon_update = 0 + + def send_imu_message(self, simulator_state: 'SimulatorState'): + for _ in range(5): + dat = messaging.new_message('accelerometer') + dat.accelerometer.sensor = 4 + dat.accelerometer.type = 0x10 + dat.accelerometer.timestamp = dat.logMonoTime # TODO: use the IMU timestamp + dat.accelerometer.init('acceleration') + dat.accelerometer.acceleration.v = [simulator_state.imu.accelerometer.x, simulator_state.imu.accelerometer.y, simulator_state.imu.accelerometer.z] + self.pm.send('accelerometer', dat) + + # copied these numbers from locationd + dat = messaging.new_message('gyroscope') + dat.gyroscope.sensor = 5 + dat.gyroscope.type = 0x10 + dat.gyroscope.timestamp = dat.logMonoTime # TODO: use the IMU timestamp + dat.gyroscope.init('gyroUncalibrated') + dat.gyroscope.gyroUncalibrated.v = [simulator_state.imu.gyroscope.x, simulator_state.imu.gyroscope.y, simulator_state.imu.gyroscope.z] + self.pm.send('gyroscope', dat) + + def send_gps_message(self, simulator_state: 'SimulatorState'): + if not simulator_state.valid: + return + + # transform vel from carla to NED + # north is -Y in CARLA + velNED = [ + -simulator_state.velocity.y, # north/south component of NED is negative when moving south + simulator_state.velocity.x, # positive when moving east, which is x in carla + simulator_state.velocity.z, + ] + + for _ in range(10): + dat = messaging.new_message('gpsLocationExternal') + dat.gpsLocationExternal = { + "unixTimestampMillis": int(time.time() * 1000), + "flags": 1, # valid fix + "accuracy": 1.0, + "verticalAccuracy": 1.0, + "speedAccuracy": 0.1, + "bearingAccuracyDeg": 0.1, + "vNED": velNED, + "bearingDeg": simulator_state.imu.bearing, + "latitude": simulator_state.gps.latitude, + "longitude": simulator_state.gps.longitude, + "altitude": simulator_state.gps.altitude, + "speed": simulator_state.speed, + "source": log.GpsLocationData.SensorSource.ublox, + } + + self.pm.send('gpsLocationExternal', dat) + + def send_peripheral_state(self): + dat = messaging.new_message('peripheralState') + dat.valid = True + dat.peripheralState = { + 'pandaType': log.PandaState.PandaType.blackPanda, + 'voltage': 12000, + 'current': 5678, + 'fanSpeedRpm': 1000 + } + Params().put_bool("ObdMultiplexingEnabled", False) + self.pm.send('peripheralState', dat) + + def send_fake_driver_monitoring(self): + # dmonitoringmodeld output + dat = messaging.new_message('driverStateV2') + dat.driverStateV2.leftDriverData.faceOrientation = [0., 0., 0.] + dat.driverStateV2.leftDriverData.faceProb = 1.0 + dat.driverStateV2.rightDriverData.faceOrientation = [0., 0., 0.] + dat.driverStateV2.rightDriverData.faceProb = 1.0 + self.pm.send('driverStateV2', dat) + + # dmonitoringd output + dat = messaging.new_message('driverMonitoringState') + dat.driverMonitoringState = { + "faceDetected": True, + "isDistracted": False, + "awarenessStatus": 1., + } + self.pm.send('driverMonitoringState', dat) + + def send_camera_images(self, world: 'World'): + with world.image_lock: + yuv = self.camerad.rgb_to_yuv(world.road_image) + self.camerad.cam_send_yuv_road(yuv) + + if world.dual_camera: + yuv = self.camerad.rgb_to_yuv(world.wide_road_image) + self.camerad.cam_send_yuv_wide_road(yuv) + + def update(self, simulator_state: 'SimulatorState', world: 'World'): + now = time.time() + self.send_imu_message(simulator_state) + self.send_gps_message(simulator_state) + + if (now - self.last_dmon_update) > DT_DMON/2: + self.send_fake_driver_monitoring() + self.last_dmon_update = now + + if (now - self.last_perp_update) > 0.25: + self.send_peripheral_state() + self.last_perp_update = now \ No newline at end of file diff --git a/tools/sim/run_bridge.py b/tools/sim/run_bridge.py new file mode 100755 index 0000000000..99617b181e --- /dev/null +++ b/tools/sim/run_bridge.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +import argparse +import os + +from typing import Any +from multiprocessing import Queue + +from openpilot.tools.sim.bridge.common import SimulatorBridge +from openpilot.tools.sim.bridge.carla.carla_bridge import CarlaBridge +from openpilot.tools.sim.bridge.metadrive.metadrive_bridge import MetaDriveBridge + + +def parse_args(add_args=None): + parser = argparse.ArgumentParser(description='Bridge between the simulator and openpilot.') + parser.add_argument('--joystick', action='store_true') + parser.add_argument('--high_quality', action='store_true') + parser.add_argument('--dual_camera', action='store_true') + parser.add_argument('--simulator', dest='simulator', type=str, default='metadrive') + + # Carla specific + parser.add_argument('--town', type=str, default='Town04_Opt') + parser.add_argument('--spawn_point', dest='num_selected_spawn_point', type=int, default=16) + parser.add_argument('--host', dest='host', type=str, default=os.environ.get("CARLA_HOST", '127.0.0.1')) + parser.add_argument('--port', dest='port', type=int, default=2000) + + return parser.parse_args(add_args) + +if __name__ == "__main__": + q: Any = Queue() + args = parse_args() + + simulator_bridge: SimulatorBridge + if args.simulator == "carla": + simulator_bridge = CarlaBridge(args) + elif args.simulator == "metadrive": + simulator_bridge = MetaDriveBridge(args) + else: + raise AssertionError("simulator type not supported") + p = simulator_bridge.run(q) + + if args.joystick: + # start input poll for joystick + from openpilot.tools.sim.lib.manual_ctrl import wheel_poll_thread + + wheel_poll_thread(q) + else: + # start input poll for keyboard + from openpilot.tools.sim.lib.keyboard_ctrl import keyboard_poll_thread + + keyboard_poll_thread(q) + + simulator_bridge.shutdown() + + p.join() diff --git a/tools/sim/start_openpilot_docker.sh b/tools/sim/start_openpilot_docker.sh index dba89b8b70..54f4995e45 100755 --- a/tools/sim/start_openpilot_docker.sh +++ b/tools/sim/start_openpilot_docker.sh @@ -3,7 +3,7 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" cd $DIR -OPENPILOT_DIR="/openpilot" +OPENPILOT_DIR="/tmp/openpilot" if ! [[ -z "$MOUNT_OPENPILOT" ]]; then OPENPILOT_DIR="$(dirname $(dirname $DIR))" EXTRA_ARGS="-v $OPENPILOT_DIR:$OPENPILOT_DIR -e PYTHONPATH=$OPENPILOT_DIR:$PYTHONPATH" diff --git a/tools/sim/tests/test_carla_bridge.py b/tools/sim/tests/test_carla_bridge.py new file mode 100755 index 0000000000..3a654c993a --- /dev/null +++ b/tools/sim/tests/test_carla_bridge.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import subprocess +import time +import unittest +from subprocess import Popen + +from openpilot.selfdrive.manager.helpers import unblock_stdout +from openpilot.tools.sim.run_bridge import parse_args +from openpilot.tools.sim.bridge.carla.carla_bridge import CarlaBridge +from openpilot.tools.sim.tests.test_sim_bridge import SIM_DIR, TestSimBridgeBase + +from typing import Optional + +class TestCarlaBridge(TestSimBridgeBase): + """ + Tests need Carla simulator to run + """ + carla_process: Optional[Popen] = None + + def setUp(self): + super().setUp() + + # We want to make sure that carla_sim docker isn't still running. + subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) + self.carla_process = subprocess.Popen("./start_carla.sh", cwd=SIM_DIR) + + # Too many lagging messages in bridge.py can cause a crash. This prevents it. + unblock_stdout() + # Wait 10 seconds to startup carla + time.sleep(10) + + def create_bridge(self): + return CarlaBridge(parse_args([])) + + def tearDown(self): + super().tearDown() + + # Stop carla simulator by removing docker container + subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) + if self.carla_process is not None: + self.carla_process.wait() + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/sim/tests/test_metadrive_bridge.py b/tools/sim/tests/test_metadrive_bridge.py new file mode 100755 index 0000000000..2c534656bb --- /dev/null +++ b/tools/sim/tests/test_metadrive_bridge.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import unittest + +from openpilot.tools.sim.run_bridge import parse_args +from openpilot.tools.sim.bridge.metadrive.metadrive_bridge import MetaDriveBridge +from openpilot.tools.sim.tests.test_sim_bridge import TestSimBridgeBase + + +class TestMetaDriveBridge(TestSimBridgeBase): + def create_bridge(self): + return MetaDriveBridge(parse_args([])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/sim/tests/test_carla_integration.py b/tools/sim/tests/test_sim_bridge.py old mode 100755 new mode 100644 similarity index 71% rename from tools/sim/tests/test_carla_integration.py rename to tools/sim/tests/test_sim_bridge.py index 65f5f4a30f..b00527b322 --- a/tools/sim/tests/test_carla_integration.py +++ b/tools/sim/tests/test_sim_bridge.py @@ -1,40 +1,24 @@ -#!/usr/bin/env python3 +import os import subprocess import time import unittest -import os + from multiprocessing import Queue from cereal import messaging from openpilot.common.basedir import BASEDIR -from openpilot.selfdrive.manager.helpers import unblock_stdout -from openpilot.tools.sim import bridge -from openpilot.tools.sim.bridge import CarlaBridge - -CI = "CI" in os.environ SIM_DIR = os.path.join(BASEDIR, "tools/sim") -class TestCarlaIntegration(unittest.TestCase): - """ - Tests need Carla simulator to run - """ - processes = None - carla_process = None +class TestSimBridgeBase(unittest.TestCase): + @classmethod + def setUpClass(cls): + if cls is TestSimBridgeBase: + raise unittest.SkipTest("Don't run this base class, run test_carla_bridge.py instead") def setUp(self): self.processes = [] - if not CI: - # We want to make sure that carla_sim docker isn't still running. - subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) - self.carla_process = subprocess.Popen("./start_carla.sh", cwd=SIM_DIR) - - # Too many lagging messages in bridge.py can cause a crash. This prevents it. - unblock_stdout() - # Wait 10 seconds to startup carla - time.sleep(10) - def test_engage(self): # Startup manager and bridge.py. Check processes are running, then engage and verify. p_manager = subprocess.Popen("./launch_openpilot.sh", cwd=SIM_DIR) @@ -42,7 +26,7 @@ class TestCarlaIntegration(unittest.TestCase): sm = messaging.SubMaster(['controlsState', 'carEvents', 'managerState']) q = Queue() - carla_bridge = CarlaBridge(bridge.parse_args([])) + carla_bridge = self.create_bridge() p_bridge = carla_bridge.run(q, retries=10) self.processes.append(p_bridge) @@ -99,11 +83,6 @@ class TestCarlaIntegration(unittest.TestCase): else: p.join(15) - # Stop carla simulator by removing docker container - subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) - if self.carla_process is not None: - self.carla_process.wait() - if __name__ == "__main__": unittest.main() diff --git a/tools/sim/tmux_script.sh b/tools/sim/tmux_script.sh index 333fce304b..2a1e74958a 100755 --- a/tools/sim/tmux_script.sh +++ b/tools/sim/tmux_script.sh @@ -2,5 +2,5 @@ tmux new -d -s carla-sim tmux send-keys "./launch_openpilot.sh" ENTER tmux neww -tmux send-keys "./bridge.py $*" ENTER +tmux send-keys "./run_bridge.py $*" ENTER tmux a -t carla-sim diff --git a/tools/zookeeper/__init__.py b/tools/zookeeper/__init__.py index 42438cb209..598e0a0587 100644 --- a/tools/zookeeper/__init__.py +++ b/tools/zookeeper/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 import ft4222 import ft4222.I2CMaster