diff --git a/.github/workflows/compile.yaml b/.github/workflows/compile.yaml new file mode 100644 index 00000000..a433132f --- /dev/null +++ b/.github/workflows/compile.yaml @@ -0,0 +1,117 @@ +name: Tests + +on: + pull_request: + branches: [master, devel] + tags: ['fw-v*'] + push: + branches: [master, devel] + tags: ['fw-v*'] + +jobs: + compile: + strategy: + fail-fast: false + matrix: + os: [ubuntu-16.04, ubuntu-latest, windows-latest, macOS-latest] + board_version: [v3.6-56V] + debug: [true] + + include: + - {os: ubuntu-latest, board_version: v3.2, debug: false} + - {os: ubuntu-latest, board_version: v3.3, debug: false} + - {os: ubuntu-latest, board_version: v3.4-24V, debug: false} + - {os: ubuntu-latest, board_version: v3.4-48V, debug: false} + - {os: ubuntu-latest, board_version: v3.5-24V, debug: false} + - {os: ubuntu-latest, board_version: v3.5-48V, debug: false} + - {os: ubuntu-latest, board_version: v3.6-24V, debug: false} + - {os: ubuntu-latest, board_version: v3.6-56V, debug: false} + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + + - name: Install prerequisites (Debian) + if: startsWith(matrix.os, 'ubuntu-') + run: | + DEBIAN_VERSION="$(lsb_release --release --short)" + echo Debian version: $DEBIAN_VERSION + if [ "$DEBIAN_VERSION" -gt 9 ]; then + sudo apt-get install gcc-arm-none-eabi + else + # Ubuntu 16.04 (Debian 9) is on ARM GCC 4.9 which is too old for us + sudo add-apt-repository ppa:team-gcc-arm-embedded/ppa + sudo apt-get update + sudo apt-get install gcc-arm-embedded + fi + + if ! (apt-cache search tup | grep "^tup - "); then + sudo add-apt-repository ppa:jonathonf/tup + sudo apt-get update + fi + + sudo apt-get install tup + + sudo apt install python3 python3-yaml python3-jinja2 python3-jsonschema + + - name: Install prerequisites (macOS) + if: startsWith(matrix.os, 'macOS-') + run: | + brew install armmbed/formulae/arm-none-eabi-gcc + brew cask install osxfuse && brew install tup + pip3 install PyYAML Jinja2 jsonschema + + + - name: Cache chocolatey + uses: actions/cache@v2 + if: startsWith(matrix.os, 'windows-') + with: + path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\gcc-arm-embedded + key: ${{ runner.os }}-gcc-arm-embedded + restore-keys: | + ${{ runner.os }}-gcc-arm-embedded + + - name: Install prerequisites (Windows) + if: startsWith(matrix.os, 'windows-') + run: | + Invoke-WebRequest -Uri "http://gittup.org/tup/win32/tup-latest.zip" -OutFile ".\tup-latest.zip" + Expand-Archive ".\tup-latest.zip" -DestinationPath ".\tup-latest" -Force + echo "::add-path::$(Resolve-Path .)\tup-latest" + + choco install gcc-arm-embedded # downloads https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-win32.zip + + pip install PyYAML Jinja2 jsonschema + + - name: Prepare Compilation + run: | + # for debugging + arm-none-eabi-gcc --version + python --version + + cd ${{ github.workspace }}/Firmware + echo "CONFIG_BOARD_VERSION=${{ matrix.board_version }}" >> tup.config + echo "CONFIG_STRICT=true" >> tup.config + echo "CONFIG_DEBUG=${{ matrix.release }}" >> tup.config + tup init + tup generate ./tup_build.sh + + - name: Compile (Unix) + if: "!startsWith(matrix.os, 'windows-')" + run: | + cd ${{ github.workspace }}/Firmware + bash -xe ./tup_build.sh + + - name: Compile (Windows) + if: startsWith(matrix.os, 'windows-') + run: | + cd ${{ github.workspace }}/Firmware + mv tup_build.sh tup_build.bat # in reality this is a .bat script on windows + .\tup_build.bat + + #code-checks: + # runs-on: ubuntu-latest + # steps: + # TODO: + # - check if enums.py is consistent with yaml + # - clang-format check + # - check if interface_generator outputs the same thing with Python 3.5 and Python 3.8 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..18f8786e --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,67 @@ +name: Build and publish HTML documentation website + +on: + push: + branches: [ feature/doc_autogen ] + +jobs: + jekyll: + runs-on: ubuntu-16.04 + steps: + - uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + # Use GitHub Actions' cache for ruby and python packages to shorten build times and decrease load on servers + - name: Cache gems + uses: actions/cache@v2 + with: + path: docs/vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('docs/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-PyYAML-Jinja2-jsonschema + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install Python dependencies + run: pip install PyYAML Jinja2 jsonschema + + # Autogenerate the API reference .md files in the python in the python/python3 container + - name: Autogenerate the API reference .md files in the python container + run: | + mkdir -p docs/_api docs/_includes + python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template docs/_layouts/api_documentation_template.j2 --outputs docs/_api/#.md + python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template docs/_layouts/api_index_template.j2 --output docs/_includes/apiindex.html + + - name: Build the site in the jekyll/builder container + run: | + docker run \ + -v ${{ github.workspace }}:/srv/jekyll -e PAGES_REPO_NWO=${GITHUB_REPOSITORY} \ + ruby:2.7-buster /bin/sh -c " + chmod 777 /srv/jekyll/docs && \ + cd /srv/jekyll/docs && \ + bundle config path vendor/bundle && \ + bundle install && \ + JEKYLL_ENV=production bundle exec jekyll build + " + touch .nojekyll + + - name: Push to documentation branch + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + git add -f docs/_site + git commit -m "jekyll build from Action ${GITHUB_SHA}" + git push --force origin HEAD:${REMOTE_BRANCH} + env: + REMOTE_BRANCH: gh-pages diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml new file mode 100644 index 00000000..4b3e3294 --- /dev/null +++ b/.github/workflows/nightly.yaml @@ -0,0 +1,34 @@ +name: pip install odrive (nightly) + +on: + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC + +jobs: + nightly: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + #pip: [pip2, pip3] + + runs-on: ${{ matrix.os }} + steps: + - name: Install odrivetool + run: | + pip install monotonic # TODO: this is dishonest. Must be removed as soon as v0.5.0 is published! + pip install odrive + + # This one currently fails because Github Actions runs pip as non-root + #- name: Check if udev rules were set up properly + # if: matrix.os == 'ubuntu-latest' + # run: test -f /etc/udev/rules.d/91-odrive.rules + + # This step is mentioned in the user guide + - name: Add ~/.local/bin to path + if: matrix.os == 'ubuntu-latest' + run: echo "::add-path::~/.local/bin" + + - name: Launch odrivetool + # This returns a non-zero exit code if the odrivetool throws an exception + run: echo 'quit()' | odrivetool shell diff --git a/.gitignore b/.gitignore index ae3506ce..66a8990f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,6 @@ coverage.xml # Django stuff: *.log -# Sphinx documentation -docs/_build/ - # PyBuilder target/ @@ -37,7 +34,30 @@ target/ .tup tup.config -/ruby-bundle -/_site -/.bundle +# Sphinx documentation +/docs/_build/ +# Autogenerated API reference +/docs/_api +/docs/_includes/apiindex.html + +# Jekyll HTML documention and artifacts +/docs/ruby-bundle +/docs/_site +/docs/.bundle/config +/docs/.jekyll-metadata + + +*.exe + +ODrive\.config + +ODrive\.creator + +ODrive\.creator\.user + +ODrive\.files + +ODrive\.includes + +Firmware/Tests/bin/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6efeeeb9..00000000 --- a/.travis.yml +++ /dev/null @@ -1,70 +0,0 @@ -# adapted from https://github.com/andysworkshop/stm32plus/blob/master/.travis.yml - -branches: - only: - - master - - devel - - /^fw-v/ - -language: c -sudo: false - -addons: - apt: - packages: - libc6-i386 - -cache: - directories: - - "$HOME/dl" - -install: -# - export GCC_DIR=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4 -# - export GCC_ARCHIVE=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 -# - export GCC_URL=https://launchpad.net/gcc-arm-embedded/5.0/5-2015-q4-major/+download/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 -# - if [ ! -e $GCC_DIR/bin/arm-none-eabi-gcc ]; then wget $GCC_URL -O $GCC_ARCHIVE; tar xfj $GCC_ARCHIVE -C $HOME/dl; fi -# - export PATH=$PATH:$GCC_DIR/bin - -- export GCC_DIR=$HOME/dl/gcc-arm-none-eabi-7-2017-q4-major -- export GCC_ARCHIVE=$HOME/dl/gcc-arm-none-eabi-7-2017-q4-major-linux.tar.bz2 -- export GCC_URL=https://developer.arm.com/-/media/Files/downloads/gnu-rm/7-2017q4/gcc-arm-none-eabi-7-2017-q4-major-linux.tar.bz2 -- if [ ! -e $GCC_DIR/bin/arm-none-eabi-gcc ]; then wget $GCC_URL -O $GCC_ARCHIVE; tar xfj $GCC_ARCHIVE -C $HOME/dl; fi -- export PATH=$PATH:$GCC_DIR/bin - -- export TUP_VER=tup_0.7.8-3~16.04.york0_amd64 -- export TUP_DIR=$HOME/dl/$TUP_VER -- export TUP_ARCHIVE=$HOME/dl/$TUP_VER.deb -- export TUP_URL=http://ppa.launchpad.net/jonathonf/tup/ubuntu/pool/main/t/tup/$TUP_VER.deb -- if [ ! -e $TUP_DIR/bin/tup ]; then wget $TUP_URL -O $TUP_ARCHIVE; dpkg-deb -R $TUP_ARCHIVE $TUP_DIR; fi -- export PATH=$PATH:$TUP_DIR/usr/bin - -env: - # Build default configuration for each board - - CONFIG_BOARD_VERSION=v3.2 DEPLOY=v3.2 - - CONFIG_BOARD_VERSION=v3.3 DEPLOY=v3.3 - - CONFIG_BOARD_VERSION=v3.4-24V DEPLOY=v3.4-24V - - CONFIG_BOARD_VERSION=v3.4-48V DEPLOY=v3.4-48V - - CONFIG_BOARD_VERSION=v3.5-24V DEPLOY=v3.5-24V - - CONFIG_BOARD_VERSION=v3.5-48V DEPLOY=v3.5-48V - - CONFIG_BOARD_VERSION=v3.6-24V DEPLOY=v3.6-24V - - CONFIG_BOARD_VERSION=v3.6-56V DEPLOY=v3.6-56V - - # Various protocol combinations - #- CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=native-stream CONFIG_UART_PROTOCOL=native - #- CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=stdout CONFIG_UART_PROTOCOL=stdout - #- CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=none CONFIG_UART_PROTOCOL=none - -script: -- "./Firmware/build.sh" - -deploy: - provider: releases - api_key: - secure: RM66joGTn11Z5PmG7Nlj8cRcVY0w7ga5qUl2ahinbDm6jMV8OxroaXKivEa478alx9fygMHVeKdJZUlrAkRJ5OYJ1trfpW+43S3OYEnfy1nyXEXRwhgeIlb9LqdrumXVAp7TZ0Vppfom8A2ZWbxKxW3lG/EAmA4G9fnxHf0S9rF0y95YVfGrxdapTKcxvbP7Yojo53474ZI6+VYrqx8lq0JAnn4FwNT9ZJ1QASrmIw4w08f60XXv25BzndCTscvLb2qUu0AaGLbQUosde0Bb7P+aQsBVY6uSkg9MWV8gWPQjtO3u5IRR1bTshxf2kPqtzwK+SpcYrddoGN6BkKAB3lVorJIW5VguUkRmtPZ1K9+NhIztNevB2qr0ASutumNLF3aqMt19KL3A+SRx6froj5VhRHf4i/Xjm3SDLTaTcc8ZIh2PEE6scUMUMs5Mzu8LQWjInRe25MSb+pQB1mNOHmoFBtVb0J3u7Nvs8jdImN5gQWvvowWXfXRNE0ncT1YsLmevwi3q+YdEjpAIPnrD/rouY8WaqQZ/vE15JM9uwdQRqKAbzGtMaKHDk7EZ7ANTyaP+UrQ/M5cVDa0bWsWSvqSqDJMy4IVHRlirYA/5u74lXNhmA8DGDB/gFVlVCmoEzas/pnYiAE1hh4RpsYxts78Ix+wbeo1hmt7t65X8cyo= - skip_cleanup: true - file_glob: true - file: Firmware/deploy/* - on: - repo: madcowswe/ODrive - branch: master - tags: true diff --git a/Arduino/ODriveArduino/ODriveArduino.cpp b/Arduino/ODriveArduino/ODriveArduino.cpp index 3f4eab5a..465ba582 100644 --- a/Arduino/ODriveArduino/ODriveArduino.cpp +++ b/Arduino/ODriveArduino/ODriveArduino.cpp @@ -66,7 +66,7 @@ bool ODriveArduino::run_state(int axis, int requested_state, bool wait) { do { delay(100); serial_ << "r axis" << axis << ".current_state\n"; - } while (readInt() != AXIS_STATE_IDLE && --timeout_ctr > 0); + } while (readInt() != requested_state && --timeout_ctr > 0); } return timeout_ctr > 0; diff --git a/CHANGELOG.md b/CHANGELOG.md index 8102a27e..31adaf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,68 @@ # Unreleased Features Please add a note of your changes below this heading if you make a Pull Request. +# Release Candidate +## [0.5.1] - Date TBD +### Added +* Added motor `torque_constant`: units of torque are now [Nm] instead of just motor current. +* [Motor thermistors support](docs/thermistors.md) +* Enable/disable of thermistor thermal limits according `setting axis..enabled`. +* Introduced `odrive-interface.yaml` as a root source for the ODrive's API. `odrivetool` connects much faster as a side effect. +* Added torque_constant and torque_lim to motor config + +### Changed +* **`input_pos`, `input_vel`, `pos_estimate_linear`, `pos_estimate_circular`, are now in units of [turns] or [turns/s] instead of [counts] or [counts/s]** +* `axis.motor.thermal_current_lim` has been removed. Instead a new property is available `axis.motor.effective_current_lim` which contains the effective current limit including any thermal limits. +* `axis.motor.get_inverter_temp()`, `axis.motor.inverter_temp_limit_lower` and `axis.motor.inverter_temp_limit_upper` have been moved to seperate fet thermistor object under `axis.fet_thermistor`. `get_inverter_temp()` function has been renamed to `temp` and is now a read-only property. + + # Releases +## [0.5.0] - 2020-08-03 +### Added +* AC Induction Motor support. + * Tracking of rotor flux through rotor time constant + * Automatic d axis current for Maximum Torque Per Amp (MTPA) +* ASCII "w" commands now execute write hooks. +* Simplified control interface ("Input Filter" branch) + * New input variables: `input_pos`, `input_vel`, and `input_current` + * New setting `input_mode` to switch between different input behaviours + * Passthrough + * Velocity Ramp + * 2nd Order Position Filter + * Trapezoidal Trajectory Planner + * Removed `set_xxx_setpoint()` functions and made `xxx_setpoint` variables read-only +* [Preliminary support for Absolute Encoders](docs/encoders.md) +* [Preliminary support for endstops and homing](docs/endstops.md) +* [CAN Communication with CANSimple stack](can-protocol.md) +* Gain scheduling for anti-hunt when close to 0 position error +* Velocity Limiting in Current Control mode according to `vel_limit` and `vel_gain` +* Regen current limiting according to `max_regen_current`, in Amps +* DC Bus hard current limiting according to `dc_max_negative_current` and `dc_max_positive_current` +* Brake resistor logic now attempts to clamp voltage according to `odrv.config.dc_bus_overvoltage_ramp_start` and `odrv.config.dc_bus_overvoltage_ramp_end` +* Unit Testing with Doctest has been started for select algorithms, see [Firmware/Tests/test_runner.cpp](Firmware/Tests/test_runner.cpp) +* Added support for Flylint VSCode Extension for static code analysis +* Using an STM32F405 .svd file allows CortexDebug to view registers during debugging +* Added scripts for building via docker. +* Added ability to change uart baudrate via fibre + +### Changed +* Changed ratiometric `motor.config.current_lim_tolerance` to absolute `motor.config.current_lim_margin` +* Moved `controller.vel_ramp_enable` to INPUT_MODE_VEL_RAMP. +* Anticogging map is temporarily forced to 0.1 deg precision, but saves with the config +* Some Encoder settings have been made read-only +* Cleaned up VSCode C/C++ Configuration settings on Windows with recursive includePath +* Now compiling with C++17 +* Fixed a firmware hang that could occur from unlikely but possible user input +* Added JSON caching to Fibre. This drastically reduces the time odrivetool needs to connect to an ODrive (except for the first time or after firmware updates). +* Fix IPython `RuntimeWarning` that would occur every time `odrivetool` was started. +* Reboot on `erase_configuration()`. This avoids unexpected behavior of a subsequent `save_configuration()` call, since the configuration is only erased from NVM, not from RAM. +* Change `motor.get_inverter_temp()` to use a property which was already being sampled at `motor.inverter_temp` +* Fixed a numerical issue in the trajectory planner that could cause sudden jumps of the position setpoint + +## [0.4.12] - 2020-05-06 +### Fixed +* Fixed a numerical issue in the trajectory planner that could cause sudden jumps of the position setpoint + ## [0.4.11] - 2019-07-25 ### Added * Separate lockin configs for sensorless, index search, and general. @@ -26,6 +87,8 @@ Please add a note of your changes below this heading if you make a Pull Request. * Script to enable using a hall signal as index edge. ### Changed +* Moved `traptraj.A_per_css` to `controller.inertia` +* Refactored velocity ramp mode into the new general input filtering structure * Encoder index search now based on the new lock-in drive feature ### Fixed @@ -101,6 +164,8 @@ Please add a note of your changes below this heading if you make a Pull Request. ## [0.4.3] - 2018-08-30 ### Added +* `min_endstop` and `max_endstop` objects can be configured on GPIO +* Axes can be homed if `min_endstop` is enabled * Encoder position count "homed" to zero when index is found. ### Changed diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9739928c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:bionic + +# Prepare the build environment and dependencies +RUN apt-get update +RUN apt-get -y install software-properties-common +RUN add-apt-repository ppa:team-gcc-arm-embedded/ppa +RUN add-apt-repository ppa:jonathonf/tup +RUN apt-get update +RUN apt-get -y upgrade +RUN apt-get -y install gcc-arm-embedded openocd tup python3.7 build-essential git + +# Build step below does not know about debian's python naming schemme +RUN ln -s /usr/bin/python3.7 /usr/bin/python + +# Copy the firmware tree into the container +RUN mkdir ODrive +COPY . ODrive +WORKDIR ODrive/Firmware + +# Hack around Tup's dependency on FUSE +RUN tup generate build.sh +RUN ./build.sh diff --git a/Firmware/.clang-format b/Firmware/.clang-format index eac50873..bad55d06 100644 --- a/Firmware/.clang-format +++ b/Firmware/.clang-format @@ -1,7 +1,6 @@ --- BasedOnStyle: Google -AlignConsecutiveAssignments: 'true' AllowShortCaseLabelsOnASingleLine: 'true' IndentWidth: '4' - +ColumnLimit: '0' ... diff --git a/Firmware/.gitignore b/Firmware/.gitignore index 496462db..a4c86dc2 100644 --- a/Firmware/.gitignore +++ b/Firmware/.gitignore @@ -1,5 +1,6 @@ #build folder +autogen/ build/ deploy/ .dep/ diff --git a/Firmware/.vscode/c_cpp_properties.json b/Firmware/.vscode/c_cpp_properties.json index c4a2f37c..1d5ab7cf 100644 --- a/Firmware/.vscode/c_cpp_properties.json +++ b/Firmware/.vscode/c_cpp_properties.json @@ -3,21 +3,7 @@ { "name": "Win32", "includePath": [ - "${workspaceRoot}", - "${workspaceRoot}/fibre/cpp/include/**", - "${workspaceRoot}/MotorControl", - "${workspaceRoot}/communication", - "${workspaceRoot}/Drivers/DRV8301", - "${workspaceRoot}/Board/v3/Inc", - "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", - "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F" + "${workspaceFolder}/**" ], "defines": [ "STM32F405xx", @@ -30,75 +16,35 @@ "__packed=\"__attribute__((__packed__))\"", "__GNUC__" ], - "intelliSenseMode": "clang-x64", - "compilerPath": "\"${ARM_GCC_ROOT}/bin/arm-none-eabi-gcc.exe\" -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float", + "intelliSenseMode": "gcc-x64", + "compilerPath": "\"${ARM_GCC_ROOT}/bin/arm-none-eabi-g++.exe\" -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float", "cStandard": "c11", - "cppStandard": "c++14" + "cppStandard": "c++17" }, { "name": "Linux", "includePath": [ - "${workspaceRoot}", - "${workspaceRoot}/fibre/cpp/include/**", - "${workspaceRoot}/MotorControl", - "${workspaceRoot}/communication", - "${workspaceRoot}/Drivers/DRV8301", - "${workspaceRoot}/Board/v3/Inc", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", - "${ARM_GCC_ROOT}/arm-none-eabi/include/**", - "${ARM_GCC_ROOT}/lib/gcc/arm-none-eabi/**" + "${workspaceFolder}/**" ], "defines": [ "STM32F405xx", "USE_HAL_DRIVER", "HW_VERSION_MAJOR=3", - "HW_VERSION_MINOR=4", - "HW_VERSION_VOLTAGE=24", + "HW_VERSION_MINOR=6", + "HW_VERSION_VOLTAGE=56", "__weak=\"__attribute__((weak))\"", "__packed=\"__attribute__((__packed__))\"", "__GNUC__" ], - "intelliSenseMode": "clang-x64", - "browse": { - "path": [ - "${workspaceRoot}", - "${ARM_GCC_ROOT}" - ], - "limitSymbolsToIncludedHeaders": true, - "databaseFilename": "" - }, - "compilerPath": "arm-none-eabi-gcc -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float", + "intelliSenseMode": "gcc-x64", + "compilerPath": "arm-none-eabi-g++ -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float", "cStandard": "c11", - "cppStandard": "c++14" + "cppStandard": "c++17" }, { "name": "Mac", "includePath": [ - "${workspaceRoot}", - "${workspaceRoot}/fibre/cpp/include/**", - "${workspaceRoot}/MotorControl", - "${workspaceRoot}/communication", - "${workspaceRoot}/Drivers/DRV8301", - "${workspaceRoot}/Board/v3/Inc", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", - "${ARM_GCC_ROOT}/arm-none-eabi/include/**", - "${ARM_GCC_ROOT}/lib/gcc/arm-none-eabi/**" + "${workspaceFolder}/**" ], "defines": [ "STM32F405xx", @@ -110,15 +56,7 @@ "__packed=\"__attribute__((__packed__))\"", "__GNUC__" ], - "intelliSenseMode": "clang-x64", - "browse": { - "path": [ - "${workspaceRoot}", - "${ARM_GCC_ROOT}" - ], - "limitSymbolsToIncludedHeaders": true, - "databaseFilename": "" - }, + "intelliSenseMode": "gcc-x64", "cStandard": "c11", "cppStandard": "c++17" } diff --git a/Firmware/.vscode/launch.json b/Firmware/.vscode/launch.json index 48fecec2..cc8662d3 100644 --- a/Firmware/.vscode/launch.json +++ b/Firmware/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "cortex-debug", "servertype": "openocd", "request": "launch", - "name": "Debug ODrive", + "name": "Debug ODrive - ST-Link", "executable": "${workspaceRoot}/build/ODriveFirmware.elf", "configFiles": [ "interface/stlink-v2.cfg", @@ -23,14 +23,48 @@ "type": "cortex-debug", "servertype": "openocd", "request": "launch", - "name": "Debug ODrive - FreeRTOS", + "name": "Debug ODrive - ST-Link - FreeRTOS", "executable": "${workspaceRoot}/build/ODriveFirmware.elf", "rtos": "FreeRTOS", "configFiles": [ "interface/stlink-v2.cfg", "target/stm32f4x_stlink.cfg", ], + "svdFile": "${workspaceRoot}/Board/v3/STM32F40x.svd", "cwd": "${workspaceRoot}" }, + { + // For the Cortex-Debug extension + // ssh -t odrv -L3333:localhost:3333 bash -c "\"openocd '-f' 'interface/stlink-v2.cfg' '-f' 'target/stm32f4x_stlink.cfg'\"" + "type": "cortex-debug", + "servertype": "external", + "gdbTarget": "localhost:3333", + "preLaunchCommands": [ + "load" + ], + "request": "launch", + "name": "Debug ODrive via external server", + "executable": "${workspaceRoot}/build/ODriveFirmware.elf", + "configFiles": [ + "interface/stlink-v2.cfg", + "target/stm32f4x_stlink.cfg", + ], + "svdFile": "${workspaceRoot}/Board/v3/STM32F40x.svd", + "cwd": "${workspaceRoot}" + }, + { + // For the Cortex-Debug extensions + "type": "cortex-debug", + "servertype": "bmp", + "request": "launch", + "name": "Debug ODrive - Black Magic Probe", + "executable": "${workspaceRoot}/build/ODriveFirmware.elf", + "device": "STM32F4xx", + "BMPGDBSerialPort": "${env:BMP_PORT}", + "interface": "swd", + "targetId": 1, + "armToolchainPath": "${env:ARM_GCC_ROOT}/bin/", + "cwd": "${workspaceRoot}" + } ] } \ No newline at end of file diff --git a/Firmware/.vscode/settings.json b/Firmware/.vscode/settings.json index 39c28f82..b0396fc7 100644 --- a/Firmware/.vscode/settings.json +++ b/Firmware/.vscode/settings.json @@ -1,9 +1,6 @@ { "C_Cpp.intelliSenseEngine": "Default", "C_Cpp.intelliSenseEngineFallback": "Disabled", - "files.exclude": { - "build": true - }, "files.associations": { "memory": "cpp", "utility": "cpp", diff --git a/Firmware/.vscode/tasks.json b/Firmware/.vscode/tasks.json index 2376b650..c82adc83 100644 --- a/Firmware/.vscode/tasks.json +++ b/Firmware/.vscode/tasks.json @@ -4,7 +4,7 @@ "version": "2.0.0", "tasks": [ { - "taskName": "build", + "label": "build", "type": "shell", "command": "make", "group": { @@ -19,13 +19,19 @@ ] }, { - "taskName": "flash", + "label": "flash - ST-Link", "type": "shell", "command": "make flash", "problemMatcher": [] }, { - "taskName": "openocd", + "label": "flash - Black Magic Probe", + "type": "shell", + "command": "make flashbmp", + "problemMatcher": [] + }, + { + "label": "openocd", "type": "shell", "command": "openocd -f \"interface/stlink-v2.cfg\" -f \"target/stm32f4x_stlink.cfg\" -c \"gdb_port 3333; log_output openocd.log\"", "problemMatcher": [] diff --git a/Firmware/Board/v3/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_usb.c b/Firmware/Board/v3/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_usb.c index 61253fca..b20415d2 100644 --- a/Firmware/Board/v3/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_usb.c +++ b/Firmware/Board/v3/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_usb.c @@ -56,6 +56,20 @@ /* Includes ------------------------------------------------------------------*/ #include "stm32f4xx_hal.h" +// taken from https://github.com/ARMmbed/mbed-os/blob/master/cmsis/TARGET_CORTEX_M/cmsis_compiler.h +#ifndef __PACKED_STRUCT + #define __PACKED_STRUCT struct __attribute__((packed)) +#endif +#ifndef __UNALIGNED_UINT32_WRITE + __PACKED_STRUCT T_UINT32_WRITE { uint32_t v; }; + #define __UNALIGNED_UINT32_WRITE(addr, val) (void)((((struct T_UINT32_WRITE *)(void *)(addr))->v) = (val)) +#endif +#ifndef __UNALIGNED_UINT32_READ + __PACKED_STRUCT T_UINT32_READ { uint32_t v; }; + #define __UNALIGNED_UINT32_READ(addr) (((const struct T_UINT32_READ *)(const void *)(addr))->v) +#endif + + /** @addtogroup STM32F4xx_LL_USB_DRIVER * @{ */ @@ -883,7 +897,7 @@ HAL_StatusTypeDef USB_WritePacket(USB_OTG_GlobalTypeDef *USBx, uint8_t *src, uin count32b = (len + 3U) / 4U; for (i = 0U; i < count32b; i++, src += 4U) { - USBx_DFIFO(ch_ep_num) = *((__packed uint32_t *)src); + USBx_DFIFO(ch_ep_num) = __UNALIGNED_UINT32_READ(src); } } return HAL_OK; @@ -909,7 +923,7 @@ void *USB_ReadPacket(USB_OTG_GlobalTypeDef *USBx, uint8_t *dest, uint16_t len) for ( i = 0U; i < count32b; i++, dest += 4U ) { - *(__packed uint32_t *)dest = USBx_DFIFO(0U); + __UNALIGNED_UINT32_WRITE(dest, USBx_DFIFO(0U)); } return ((void *)dest); diff --git a/Firmware/Board/v3/Inc/freertos_vars.h b/Firmware/Board/v3/Inc/freertos_vars.h index 6982ee28..7e86c971 100644 --- a/Firmware/Board/v3/Inc/freertos_vars.h +++ b/Firmware/Board/v3/Inc/freertos_vars.h @@ -7,8 +7,11 @@ extern osSemaphoreId sem_usb_irq; extern osSemaphoreId sem_uart_dma; extern osSemaphoreId sem_usb_rx; extern osSemaphoreId sem_usb_tx; +extern osSemaphoreId sem_can; extern osThreadId defaultTaskHandle; extern osThreadId usb_irq_thread; +extern const uint32_t stack_size_usb_irq_thread; +extern const uint32_t stack_size_default_task; #endif /* __FREERTOS_H */ \ No newline at end of file diff --git a/Firmware/Board/v3/Inc/gpio.h b/Firmware/Board/v3/Inc/gpio.h index 7c7a4e63..81f90be9 100644 --- a/Firmware/Board/v3/Inc/gpio.h +++ b/Firmware/Board/v3/Inc/gpio.h @@ -72,13 +72,10 @@ void MX_GPIO_Init(void); void SetGPIO12toUART(); bool GPIO_subscribe(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin, - uint32_t pull_up_down, - void (*callback)(void*), void* ctx); + uint32_t pull_up_down, void (*callback)(void*), void* ctx); void GPIO_unsubscribe(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin); void GPIO_set_to_analog(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin); -uint16_t get_gpio_pin_by_pin(uint16_t GPIO_pin); -GPIO_TypeDef* get_gpio_port_by_pin(uint16_t GPIO_pin); #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR <= 4 #define GPIO_COUNT 5 diff --git a/Firmware/Board/v3/Inc/main.h b/Firmware/Board/v3/Inc/main.h index bfd9888c..6f663846 100644 --- a/Firmware/Board/v3/Inc/main.h +++ b/Firmware/Board/v3/Inc/main.h @@ -173,10 +173,8 @@ #if HW_VERSION_VOLTAGE >= 48 #define VBUS_S_DIVIDER_RATIO 19.0f -#define VBUS_OVERVOLTAGE_LEVEL 52.0f #elif HW_VERSION_VOLTAGE == 24 #define VBUS_S_DIVIDER_RATIO 11.0f -#define VBUS_OVERVOLTAGE_LEVEL 26.0f #else #error "unknown board voltage" #endif diff --git a/Firmware/Board/v3/Src/can.c b/Firmware/Board/v3/Src/can.c index fc7bf388..011150ff 100644 --- a/Firmware/Board/v3/Src/can.c +++ b/Firmware/Board/v3/Src/can.c @@ -63,15 +63,15 @@ void MX_CAN1_Init(void) { hcan1.Instance = CAN1; - hcan1.Init.Prescaler = 7; + hcan1.Init.Prescaler = 8; hcan1.Init.Mode = CAN_MODE_NORMAL; - hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ; - hcan1.Init.TimeSeg1 = CAN_BS1_6TQ; - hcan1.Init.TimeSeg2 = CAN_BS2_5TQ; + hcan1.Init.SyncJumpWidth = CAN_SJW_4TQ; + hcan1.Init.TimeSeg1 = CAN_BS1_16TQ; + hcan1.Init.TimeSeg2 = CAN_BS2_4TQ; hcan1.Init.TimeTriggeredMode = DISABLE; - hcan1.Init.AutoBusOff = DISABLE; + hcan1.Init.AutoBusOff = ENABLE; hcan1.Init.AutoWakeUp = ENABLE; - hcan1.Init.AutoRetransmission = DISABLE; + hcan1.Init.AutoRetransmission = ENABLE; hcan1.Init.ReceiveFifoLocked = DISABLE; hcan1.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan1) != HAL_OK) diff --git a/Firmware/Board/v3/Src/freertos.c b/Firmware/Board/v3/Src/freertos.c index ac6c6de9..b2d49e55 100644 --- a/Firmware/Board/v3/Src/freertos.c +++ b/Firmware/Board/v3/Src/freertos.c @@ -61,6 +61,7 @@ extern PCD_HandleTypeDef hpcd_USB_OTG_FS; int odrive_main(void); int load_configuration(void); +int construct_objects(void); /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -85,14 +86,17 @@ osSemaphoreId sem_usb_irq; osSemaphoreId sem_uart_dma; osSemaphoreId sem_usb_rx; osSemaphoreId sem_usb_tx; +osSemaphoreId sem_can; osThreadId usb_irq_thread; +const uint32_t stack_size_usb_irq_thread = 2048; // Bytes // Place FreeRTOS heap in core coupled memory for better performance __attribute__((section(".ccmram"))) uint8_t ucHeap[configTOTAL_HEAP_SIZE]; /* USER CODE END Variables */ osThreadId defaultTaskHandle; +const uint32_t stack_size_default_task = 2048; // Bytes /* Private function prototypes -----------------------------------------------*/ /* USER CODE BEGIN FunctionPrototypes */ @@ -148,7 +152,7 @@ void usb_deferred_interrupt_thread(void * ctx) { void init_deferred_interrupts(void) { // Start USB interrupt handler thread - osThreadDef(task_usb_pump, usb_deferred_interrupt_thread, osPriorityAboveNormal, 0, 512); + osThreadDef(task_usb_pump, usb_deferred_interrupt_thread, osPriorityAboveNormal, 0, stack_size_usb_irq_thread / sizeof(StackType_t)); usb_irq_thread = osThreadCreate(osThread(task_usb_pump), NULL); } @@ -187,10 +191,15 @@ void MX_FREERTOS_Init(void) { osSemaphoreDef(sem_usb_tx); sem_usb_tx = osSemaphoreCreate(osSemaphore(sem_usb_tx), 1); + osSemaphoreDef(sem_can); + sem_can = osSemaphoreCreate(osSemaphore(sem_can), 1); + osSemaphoreWait(sem_can, 0); + init_deferred_interrupts(); // Load persistent configuration (or defaults) load_configuration(); + construct_objects(); /* USER CODE END RTOS_SEMAPHORES */ /* USER CODE BEGIN RTOS_TIMERS */ @@ -199,7 +208,7 @@ void MX_FREERTOS_Init(void) { /* Create the thread(s) */ /* definition and creation of defaultTask */ - osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 256); + osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, stack_size_default_task / sizeof(StackType_t)); defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL); /* USER CODE BEGIN RTOS_THREADS */ diff --git a/Firmware/Board/v3/Src/gpio.c b/Firmware/Board/v3/Src/gpio.c index 3fa36da4..635091ee 100644 --- a/Firmware/Board/v3/Src/gpio.c +++ b/Firmware/Board/v3/Src/gpio.c @@ -205,8 +205,7 @@ size_t n_subscriptions = 0; // on a rising edge of the GPIO. // @param pull_up_down: one of GPIO_NOPULL, GPIO_PULLUP or GPIO_PULLDOWN bool GPIO_subscribe(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin, - uint32_t pull_up_down, - void (*callback)(void*), void* ctx) { + uint32_t pull_up_down, void (*callback)(void*), void* ctx) { // Register handler (or reuse existing registration) // TODO: make thread safe @@ -279,49 +278,7 @@ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_pin) { } } -GPIO_TypeDef* get_gpio_port_by_pin(uint16_t GPIO_pin){ - switch(GPIO_pin){ - case 1: return GPIO_1_GPIO_Port; break; - case 2: return GPIO_2_GPIO_Port; break; - case 3: return GPIO_3_GPIO_Port; break; - case 4: return GPIO_4_GPIO_Port; break; -#ifdef GPIO_5_GPIO_Port - case 5: return GPIO_5_GPIO_Port; break; -#endif -#ifdef GPIO_6_GPIO_Port - case 6: return GPIO_6_GPIO_Port; break; -#endif -#ifdef GPIO_7_GPIO_Port - case 7: return GPIO_7_GPIO_Port; break; -#endif -#ifdef GPIO_8_GPIO_Port - case 8: return GPIO_8_GPIO_Port; break; -#endif - default: return GPIO_1_GPIO_Port; - } -} -uint16_t get_gpio_pin_by_pin(uint16_t GPIO_pin){ - switch(GPIO_pin){ - case 1: return GPIO_1_Pin; break; - case 2: return GPIO_2_Pin; break; - case 3: return GPIO_3_Pin; break; - case 4: return GPIO_4_Pin; break; -#ifdef GPIO_5_Pin - case 5: return GPIO_5_Pin; break; -#endif -#ifdef GPIO_6_Pin - case 6: return GPIO_6_Pin; break; -#endif -#ifdef GPIO_7_Pin - case 7: return GPIO_7_Pin; break; -#endif -#ifdef GPIO_8_Pin - case 8: return GPIO_8_Pin; break; -#endif - default: return GPIO_1_Pin; - } -} /* USER CODE END 2 */ diff --git a/Firmware/Board/v3/Src/spi.c b/Firmware/Board/v3/Src/spi.c index d66515f9..a3a3311e 100644 --- a/Firmware/Board/v3/Src/spi.c +++ b/Firmware/Board/v3/Src/spi.c @@ -103,7 +103,7 @@ void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) */ GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; - GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Pull = GPIO_PULLUP; // required for disconnect detection on SPI encoders GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF6_SPI3; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); @@ -115,8 +115,15 @@ void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) hdma_spi3_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_spi3_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi3_tx.Init.MemInc = DMA_MINC_ENABLE; - hdma_spi3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; - hdma_spi3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + + if(spiHandle->Init.DataSize == SPI_DATASIZE_8BIT){ + hdma_spi3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; + hdma_spi3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; + } else { + hdma_spi3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; + hdma_spi3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + } + hdma_spi3_tx.Init.Mode = DMA_NORMAL; hdma_spi3_tx.Init.Priority = DMA_PRIORITY_MEDIUM; hdma_spi3_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; @@ -133,8 +140,13 @@ void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) hdma_spi3_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_spi3_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi3_rx.Init.MemInc = DMA_MINC_ENABLE; - hdma_spi3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; - hdma_spi3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + if (spiHandle->Init.DataSize == SPI_DATASIZE_8BIT) { + hdma_spi3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; + hdma_spi3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; + } else { + hdma_spi3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; + hdma_spi3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + } hdma_spi3_rx.Init.Mode = DMA_NORMAL; hdma_spi3_rx.Init.Priority = DMA_PRIORITY_MEDIUM; hdma_spi3_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; diff --git a/Firmware/Drivers/DRV8301/drv8301.c b/Firmware/Drivers/DRV8301/drv8301.c index 0e90a652..c65cc4d3 100644 --- a/Firmware/Drivers/DRV8301/drv8301.c +++ b/Firmware/Drivers/DRV8301/drv8301.c @@ -45,7 +45,7 @@ // drivers #include "drv8301.h" -#include "utils.h" +#include "utils.hpp" // ************************************************************************** diff --git a/Firmware/Makefile b/Firmware/Makefile index 91754f5a..a40ae89f 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -20,6 +20,15 @@ flash: all -c 'reset run' \ -c exit +flashbmp: all + arm-none-eabi-gdb --ex 'target extended-remote $(BMP_PORT)' \ + --ex 'monitor swdp_scan' \ + --ex 'attach 1' \ + --ex 'load' \ + --ex 'detach' \ + --ex 'quit' \ + $(FIRMWARE) + gdb: all arm-none-eabi-gdb $(FIRMWARE) -x openocd.gdbinit diff --git a/Firmware/MotorControl/axis.cpp b/Firmware/MotorControl/axis.cpp index e5f25b16..04a802c6 100644 --- a/Firmware/MotorControl/axis.cpp +++ b/Firmware/MotorControl/axis.cpp @@ -3,8 +3,10 @@ #include #include "gpio.h" -#include "utils.h" #include "odrive_main.h" +#include "utils.hpp" +#include "gpio_utils.hpp" +#include "communication/interface_can.hpp" Axis::Axis(int axis_num, const AxisHardwareConfig_t& hw_config, @@ -12,25 +14,42 @@ Axis::Axis(int axis_num, Encoder& encoder, SensorlessEstimator& sensorless_estimator, Controller& controller, + OnboardThermistorCurrentLimiter& fet_thermistor, + OffboardThermistorCurrentLimiter& motor_thermistor, Motor& motor, - TrapezoidalTrajectory& trap) + TrapezoidalTrajectory& trap, + Endstop& min_endstop, + Endstop& max_endstop) : axis_num_(axis_num), hw_config_(hw_config), config_(config), encoder_(encoder), sensorless_estimator_(sensorless_estimator), controller_(controller), + fet_thermistor_(fet_thermistor), + motor_thermistor_(motor_thermistor), motor_(motor), - trap_(trap) + trap_traj_(trap), + min_endstop_(min_endstop), + max_endstop_(max_endstop), + current_limiters_(make_array( + static_cast(&fet_thermistor), + static_cast(&motor_thermistor))), + thermistors_(make_array( + static_cast(&fet_thermistor), + static_cast(&motor_thermistor))) { encoder_.axis_ = this; sensorless_estimator_.axis_ = this; controller_.axis_ = this; + fet_thermistor_.axis_ = this; + motor_thermistor.axis_ = this; motor_.axis_ = this; - trap_.axis_ = this; - + trap_traj_.axis_ = this; + min_endstop_.axis_ = this; + max_endstop_.axis_ = this; decode_step_dir_pins(); - update_watchdog_settings(); + watchdog_feed(); } Axis::LockinConfig_t Axis::default_calibration() { @@ -65,10 +84,10 @@ static void step_cb_wrapper(void* ctx) { reinterpret_cast(ctx)->step_cb(); } + // @brief Sets up all components of the axis, // such as gate driver and encoder hardware. void Axis::setup() { - encoder_.setup(); motor_.setup(); } @@ -79,7 +98,7 @@ static void run_state_machine_loop_wrapper(void* ctx) { // @brief Starts run_state_machine_loop in a new thread void Axis::start_thread() { - osThreadDef(thread_def, run_state_machine_loop_wrapper, hw_config_.thread_priority, 0, 4*512); + osThreadDef(thread_def, run_state_machine_loop_wrapper, hw_config_.thread_priority, 0, stack_size_ / sizeof(StackType_t)); thread_id_ = osThreadCreate(osThread(thread_def), this); thread_id_valid_ = true; } @@ -99,11 +118,10 @@ bool Axis::wait_for_current_meas() { // step/direction interface void Axis::step_cb() { - if (step_dir_active_) { - GPIO_PinState dir_pin = HAL_GPIO_ReadPin(dir_port_, dir_pin_); - float dir = (dir_pin == GPIO_PIN_SET) ? 1.0f : -1.0f; - controller_.pos_setpoint_ += dir * config_.counts_per_step; - } + const bool dir_pin = dir_port_->IDR & dir_pin_; + const int32_t dir = (-1 + 2 * dir_pin) * step_dir_active_; + controller_.input_pos_ += dir * config_.turns_per_step; + controller_.input_pos_updated(); }; void Axis::load_default_step_dir_pin_config( @@ -112,6 +130,10 @@ void Axis::load_default_step_dir_pin_config( config->dir_gpio_pin = hw_config.dir_gpio_pin; } +void Axis::load_default_can_id(const int& id, Config_t& config){ + config.can_node_id = id; +} + void Axis::decode_step_dir_pins() { step_port_ = get_gpio_port_by_pin(config_.step_gpio_pin); step_pin_ = get_gpio_pin_by_pin(config_.step_gpio_pin); @@ -119,21 +141,6 @@ void Axis::decode_step_dir_pins() { dir_pin_ = get_gpio_pin_by_pin(config_.dir_gpio_pin); } -// @brief: Setup the watchdog reset value from the configuration watchdog timeout interval. -void Axis::update_watchdog_settings() { - - if(config_.watchdog_timeout <= 0.0f) { // watchdog disabled - watchdog_reset_value_ = 0; - } else if(config_.watchdog_timeout >= UINT32_MAX / (current_meas_hz+1)) { //overflow! - watchdog_reset_value_ = UINT32_MAX; - } else { - watchdog_reset_value_ = static_cast(config_.watchdog_timeout * current_meas_hz); - } - - // Do a feed to avoid instant timeout - watchdog_feed(); -} - // @brief (de)activates step/dir input void Axis::set_step_dir_active(bool active) { if (active) { @@ -145,8 +152,7 @@ void Axis::set_step_dir_active(bool active) { HAL_GPIO_Init(dir_port_, &GPIO_InitStruct); // Subscribe to rising edges of the step GPIO - GPIO_subscribe(step_port_, step_pin_, GPIO_PULLDOWN, - step_cb_wrapper, this); + GPIO_subscribe(step_port_, step_pin_, GPIO_PULLDOWN, step_cb_wrapper, this); step_dir_active_ = true; } else { @@ -165,40 +171,56 @@ bool Axis::do_checks() { if ((current_state_ != AXIS_STATE_IDLE) && (motor_.armed_state_ == Motor::ARMED_STATE_DISARMED)) // motor got disarmed in something other than the idle loop error_ |= ERROR_MOTOR_DISARMED; - if (!(vbus_voltage >= board_config.dc_bus_undervoltage_trip_level)) + if (!(vbus_voltage >= odrv.config_.dc_bus_undervoltage_trip_level)) error_ |= ERROR_DC_BUS_UNDER_VOLTAGE; - if (!(vbus_voltage <= board_config.dc_bus_overvoltage_trip_level)) + if (!(vbus_voltage <= odrv.config_.dc_bus_overvoltage_trip_level)) error_ |= ERROR_DC_BUS_OVER_VOLTAGE; // Sub-components should use set_error which will propegate to this error_ + for (ThermistorCurrentLimiter* thermistor : thermistors_) { + thermistor->do_checks(); + } motor_.do_checks(); - encoder_.do_checks(); + // encoder_.do_checks(); // sensorless_estimator_.do_checks(); // controller_.do_checks(); + // Check for endstop presses + if (min_endstop_.config_.enabled && min_endstop_.get_state() && !(current_state_ == AXIS_STATE_HOMING)) { + error_ |= ERROR_MIN_ENDSTOP_PRESSED; + } else if (max_endstop_.config_.enabled && max_endstop_.get_state() && !(current_state_ == AXIS_STATE_HOMING)) { + error_ |= ERROR_MAX_ENDSTOP_PRESSED; + } + return check_for_errors(); } // @brief Update all esitmators bool Axis::do_updates() { // Sub-components should use set_error which will propegate to this error_ + for (ThermistorCurrentLimiter* thermistor : thermistors_) { + thermistor->update(); + } encoder_.update(); sensorless_estimator_.update(); - return check_for_errors(); + min_endstop_.update(); + max_endstop_.update(); + bool ret = check_for_errors(); + odCAN->send_heartbeat(this); + return ret; } // @brief Feed the watchdog to prevent watchdog timeouts. void Axis::watchdog_feed() { - watchdog_current_value_ = watchdog_reset_value_; + watchdog_current_value_ = get_watchdog_reset(); } -// @brief Check the watchdog timer for expiration. Also sets the watchdog error bit if expired. +// @brief Check the watchdog timer for expiration. Also sets the watchdog error bit if expired. bool Axis::watchdog_check() { - // reset value = 0 means watchdog disabled. - if(watchdog_reset_value_ == 0) return true; + if (!config_.enable_watchdog) return true; // explicit check here to ensure that we don't underflow back to UINT32_MAX - if(watchdog_current_value_ > 0) { + if (watchdog_current_value_ > 0) { watchdog_current_value_--; return true; } else { @@ -229,9 +251,9 @@ bool Axis::run_lockin_spin(const LockinConfig_t &lockin_config) { auto spin_done = [&](bool vel_override = false) -> bool { bool done = false; if (lockin_config.finish_on_vel || vel_override) - done = done || fabsf(vel) >= fabsf(lockin_config.vel); + done = done || std::abs(vel) >= std::abs(lockin_config.vel); if (lockin_config.finish_on_distance) - done = done || fabsf(distance) >= fabsf(lockin_config.finish_distance); + done = done || std::abs(distance) >= std::abs(lockin_config.finish_distance); if (lockin_config.finish_on_enc_idx) done = done || encoder_.index_found_; return done; @@ -272,15 +294,18 @@ bool Axis::run_lockin_spin(const LockinConfig_t &lockin_config) { // Note run_sensorless_control_loop and run_closed_loop_control_loop are very similar and differ only in where we get the estimate from. bool Axis::run_sensorless_control_loop() { - run_control_loop([this](){ - if (controller_.config_.control_mode >= Controller::CTRL_MODE_POSITION_CONTROL) - return error_ |= ERROR_POS_CTRL_DURING_SENSORLESS, false; + controller_.pos_estimate_linear_src_ = nullptr; + controller_.pos_estimate_circular_src_ = nullptr; + controller_.pos_estimate_valid_src_ = nullptr; + controller_.vel_estimate_src_ = &sensorless_estimator_.vel_estimate_; + controller_.vel_estimate_valid_src_ = &sensorless_estimator_.vel_estimate_valid_; + run_control_loop([this](){ // Note that all estimators are updated in the loop prefix in run_control_loop - float current_setpoint; - if (!controller_.update(sensorless_estimator_.pll_pos_, sensorless_estimator_.vel_estimate_, ¤t_setpoint)) + float torque_setpoint; + if (!controller_.update(&torque_setpoint)) return error_ |= ERROR_CONTROLLER_FAILED, false; - if (!motor_.update(current_setpoint, sensorless_estimator_.phase_, sensorless_estimator_.vel_estimate_)) + if (!motor_.update(torque_setpoint, sensorless_estimator_.phase_, sensorless_estimator_.vel_estimate_)) return false; // set_error should update axis.error_ return true; }); @@ -288,20 +313,146 @@ bool Axis::run_sensorless_control_loop() { } bool Axis::run_closed_loop_control_loop() { + if (!controller_.select_encoder(controller_.config_.load_encoder_axis)) { + return error_ |= ERROR_CONTROLLER_FAILED, false; + } + // To avoid any transient on startup, we intialize the setpoint to be the current position - controller_.pos_setpoint_ = encoder_.pos_estimate_; + if (controller_.config_.circular_setpoints) { + if (!controller_.pos_estimate_circular_src_) { + return error_ |= ERROR_CONTROLLER_FAILED, false; + } + else { + controller_.pos_setpoint_ = *controller_.pos_estimate_circular_src_; + controller_.input_pos_ = *controller_.pos_estimate_circular_src_; + } + } + else { + if (!controller_.pos_estimate_linear_src_) { + return error_ |= ERROR_CONTROLLER_FAILED, false; + } + else { + controller_.pos_setpoint_ = *controller_.pos_estimate_linear_src_; + controller_.input_pos_ = *controller_.pos_estimate_linear_src_; + } + } + controller_.input_pos_updated(); + + // Avoid integrator windup issues + controller_.vel_integrator_torque_ = 0.0f; + set_step_dir_active(config_.enable_step_dir); run_control_loop([this](){ // Note that all estimators are updated in the loop prefix in run_control_loop - float current_setpoint; - if (!controller_.update(encoder_.pos_estimate_, encoder_.vel_estimate_, ¤t_setpoint)) - return error_ |= ERROR_CONTROLLER_FAILED, false; //TODO: Make controller.set_error - float phase_vel = 2*M_PI * encoder_.vel_estimate_ / (float)encoder_.config_.cpr * motor_.config_.pole_pairs; - if (!motor_.update(current_setpoint, encoder_.phase_, phase_vel)) + float torque_setpoint; + if (!controller_.update(&torque_setpoint)) + return error_ |= ERROR_CONTROLLER_FAILED, false; + + float phase_vel = (2*M_PI) * encoder_.vel_estimate_ * motor_.config_.pole_pairs; + if (!motor_.update(torque_setpoint, encoder_.phase_, phase_vel)) return false; // set_error should update axis.error_ + return true; }); - set_step_dir_active(false); + set_step_dir_active(config_.enable_step_dir && config_.step_dir_always_on); + return check_for_errors(); +} + + +// Slowly drive in the negative direction at homing_speed until the min endstop is pressed +// When pressed, set the linear count to the offset (default 0), and then go to position 0 +bool Axis::run_homing() { + Controller::ControlMode stored_control_mode = controller_.config_.control_mode; + Controller::InputMode stored_input_mode = controller_.config_.input_mode; + + // TODO: theoretically this check should be inside the update loop, + // otherwise someone could disable the endstop while homing is in progress. + if (!min_endstop_.config_.enabled) { + return error_ |= ERROR_HOMING_WITHOUT_ENDSTOP, false; + } + + controller_.config_.control_mode = Controller::CONTROL_MODE_VELOCITY_CONTROL; + controller_.config_.input_mode = Controller::INPUT_MODE_VEL_RAMP; + + controller_.input_pos_ = 0.0f; + controller_.input_pos_updated(); + controller_.input_vel_ = -controller_.config_.homing_speed; + controller_.input_torque_ = 0.0f; + + homing_.is_homed = false; + + if (!controller_.select_encoder(controller_.config_.load_encoder_axis)) { + return error_ |= ERROR_CONTROLLER_FAILED, false; + } + + // To avoid any transient on startup, we intialize the setpoint to be the current position + // note - input_pos_ is not set here. It is set to 0 earlier in this method and velocity control is used. + if (controller_.config_.circular_setpoints) { + if (!controller_.pos_estimate_circular_src_) { + return error_ |= ERROR_CONTROLLER_FAILED, false; + } + else { + controller_.pos_setpoint_ = *controller_.pos_estimate_circular_src_; + } + } + else { + if (!controller_.pos_estimate_linear_src_) { + return error_ |= ERROR_CONTROLLER_FAILED, false; + } + else { + controller_.pos_setpoint_ = *controller_.pos_estimate_linear_src_; + } + } + + // Avoid integrator windup issues + controller_.vel_integrator_torque_ = 0.0f; + + run_control_loop([this](){ + // Note that all estimators are updated in the loop prefix in run_control_loop + float torque_setpoint; + if (!controller_.update(&torque_setpoint)) + return error_ |= ERROR_CONTROLLER_FAILED, false; + + float phase_vel = (2*M_PI) * encoder_.vel_estimate_ * motor_.config_.pole_pairs; + if (!motor_.update(torque_setpoint, encoder_.phase_, phase_vel)) + return false; // set_error should update axis.error_ + + return !min_endstop_.get_state(); + }); + error_ &= ~ERROR_MIN_ENDSTOP_PRESSED; // clear this error since we deliberately drove into the endstop + + // pos_setpoint is the starting position for the trap_traj so we need to set it. + controller_.pos_setpoint_ = min_endstop_.config_.offset; + controller_.vel_setpoint_ = 0.0f; // Change directions without decelerating + + // Set our current position in encoder counts to make control more logical + encoder_.set_linear_count((int32_t)controller_.pos_setpoint_); + + controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL; + controller_.config_.input_mode = Controller::INPUT_MODE_TRAP_TRAJ; + + controller_.input_pos_ = 0.0f; + controller_.input_pos_updated(); + controller_.input_vel_ = 0.0f; + controller_.input_torque_ = 0.0f; + + run_control_loop([this](){ + // Note that all estimators are updated in the loop prefix in run_control_loop + float torque_setpoint; + if (!controller_.update(&torque_setpoint)) + return error_ |= ERROR_CONTROLLER_FAILED, false; + + float phase_vel = (2*M_PI) * encoder_.vel_estimate_ * motor_.config_.pole_pairs; + if (!motor_.update(torque_setpoint, encoder_.phase_, phase_vel)) + return false; // set_error should update axis.error_ + + return !controller_.trajectory_done_; + }); + + controller_.config_.control_mode = stored_control_mode; + controller_.config_.input_mode = stored_input_mode; + homing_.is_homed = true; + return check_for_errors(); } @@ -309,7 +460,8 @@ bool Axis::run_idle_loop() { // run_control_loop ignores missed modulation timing updates // if and only if we're in AXIS_STATE_IDLE safety_critical_disarm_motor_pwm(motor_); - run_control_loop([this](){ + set_step_dir_active(config_.enable_step_dir && config_.step_dir_always_on); + run_control_loop([this]() { return true; }); return check_for_errors(); @@ -318,20 +470,9 @@ bool Axis::run_idle_loop() { // Infinite loop that does calibration and enters main control loop as appropriate void Axis::run_state_machine_loop() { - // Allocate the map for anti-cogging algorithm and initialize all values to 0.0f - // TODO: Move this somewhere else - // TODO: respect changes of CPR - int encoder_cpr = encoder_.config_.cpr; - controller_.anticogging_.cogging_map = (float*)malloc(encoder_cpr * sizeof(float)); - if (controller_.anticogging_.cogging_map != NULL) { - for (int i = 0; i < encoder_cpr; i++) { - controller_.anticogging_.cogging_map[i] = 0.0f; - } - } - // arm! motor_.arm(); - + for (;;) { // Load the task chain if a specific request is pending if (requested_state_ != AXIS_STATE_UNDEFINED) { @@ -343,6 +484,8 @@ void Axis::run_state_machine_loop() { task_chain_[pos++] = AXIS_STATE_ENCODER_INDEX_SEARCH; if (config_.startup_encoder_offset_calibration) task_chain_[pos++] = AXIS_STATE_ENCODER_OFFSET_CALIBRATION; + if (config_.startup_homing) + task_chain_[pos++] = AXIS_STATE_HOMING; if (config_.startup_closed_loop_control) task_chain_[pos++] = AXIS_STATE_CLOSED_LOOP_CONTROL; else if (config_.startup_sensorless_control) @@ -358,7 +501,7 @@ void Axis::run_state_machine_loop() { task_chain_[pos++] = requested_state_; task_chain_[pos++] = AXIS_STATE_IDLE; } - task_chain_[pos++] = AXIS_STATE_UNDEFINED; // TODO: bounds checking + task_chain_[pos++] = AXIS_STATE_UNDEFINED; // TODO: bounds checking requested_state_ = AXIS_STATE_UNDEFINED; // Auto-clear any invalid state error error_ &= ~ERROR_INVALID_STATE; @@ -390,6 +533,10 @@ void Axis::run_state_machine_loop() { status = encoder_.run_direction_find(); } break; + case AXIS_STATE_HOMING: { + status = run_homing(); + } break; + case AXIS_STATE_ENCODER_OFFSET_CALIBRATION: { if (!motor_.is_calibrated_) goto invalid_state_label; @@ -399,7 +546,7 @@ void Axis::run_state_machine_loop() { case AXIS_STATE_LOCKIN_SPIN: { if (!motor_.is_calibrated_ || motor_.config_.direction==0) goto invalid_state_label; - status = run_lockin_spin(config_.lockin); + status = run_lockin_spin(config_.general_lockin); } break; case AXIS_STATE_SENSORLESS_CONTROL: { @@ -419,6 +566,7 @@ void Axis::run_state_machine_loop() { goto invalid_state_label; if (!encoder_.is_ready_) goto invalid_state_label; + watchdog_feed(); status = run_closed_loop_control_loop(); } break; @@ -430,14 +578,17 @@ void Axis::run_state_machine_loop() { default: invalid_state_label: error_ |= ERROR_INVALID_STATE; - status = false; // this will set the state to idle + status = false; // this will set the state to idle break; } // If the state failed, go to idle, else advance task chain - if (!status) + if (!status) { + std::fill(task_chain_.begin(), task_chain_.end(), AXIS_STATE_UNDEFINED); current_state_ = AXIS_STATE_IDLE; - else - memcpy(task_chain_, task_chain_ + 1, sizeof(task_chain_) - sizeof(task_chain_[0])); + } else { + std::rotate(task_chain_.begin(), task_chain_.begin() + 1, task_chain_.end()); + task_chain_.back() = AXIS_STATE_UNDEFINED; + } } } diff --git a/Firmware/MotorControl/axis.hpp b/Firmware/MotorControl/axis.hpp index c23fb2f1..446958d6 100644 --- a/Firmware/MotorControl/axis.hpp +++ b/Firmware/MotorControl/axis.hpp @@ -5,38 +5,10 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Axis { +#include + +class Axis : public ODriveIntf::AxisIntf { public: - enum Error_t { - ERROR_NONE = 0x00, - ERROR_INVALID_STATE = 0x01, //decode_step_dir_pins(); } + void set_dir_gpio_pin(uint16_t value) { dir_gpio_pin = value; parent->decode_step_dir_pins(); } + }; + + struct Homing_t { + bool is_homed = false; }; enum thread_signals { M_SIGNAL_PH_CURRENT_MEAS = 1u << 0 }; - enum LockinState_t { - LOCKIN_STATE_INACTIVE, - LOCKIN_STATE_RAMP, - LOCKIN_STATE_ACCELERATE, - LOCKIN_STATE_CONST_VEL, - }; - Axis(int axis_num, const AxisHardwareConfig_t& hw_config, Config_t& config, Encoder& encoder, SensorlessEstimator& sensorless_estimator, Controller& controller, + OnboardThermistorCurrentLimiter& fet_thermistor, + OffboardThermistorCurrentLimiter& motor_thermistor, Motor& motor, - TrapezoidalTrajectory& trap); + TrapezoidalTrajectory& trap, + Endstop& min_endstop, + Endstop& max_endstop); void setup(); void start_thread(); @@ -103,10 +92,10 @@ public: void step_cb(); void set_step_dir_active(bool enable); void decode_step_dir_pins(); - void update_watchdog_settings(); static void load_default_step_dir_pin_config( const AxisHardwareConfig_t& hw_config, Config_t* config); + static void load_default_can_id(const int& id, Config_t& config); bool check_DRV_fault(); bool check_PSU_brownout(); @@ -116,6 +105,15 @@ public: void watchdog_feed(); bool watchdog_check(); + void clear_errors() { + motor_.error_ = Motor::ERROR_NONE; + controller_.error_ = Controller::ERROR_NONE; + sensorless_estimator_.error_ = SensorlessEstimator::ERROR_NONE; + encoder_.error_ = Encoder::ERROR_NONE; + encoder_.spi_error_rate_ = 0.0f; + + error_ = ERROR_NONE; + } // True if there are no errors bool inline check_for_errors() { @@ -186,8 +184,13 @@ public: bool run_lockin_spin(const LockinConfig_t &lockin_config); bool run_sensorless_control_loop(); bool run_closed_loop_control_loop(); + bool run_homing(); bool run_idle_loop(); + constexpr uint32_t get_watchdog_reset() { + return static_cast(std::clamp(config_.watchdog_timeout, 0, UINT32_MAX / (current_meas_hz + 1)) * current_meas_hz); + } + void run_state_machine_loop(); int axis_num_; @@ -197,14 +200,24 @@ public: Encoder& encoder_; SensorlessEstimator& sensorless_estimator_; Controller& controller_; + OnboardThermistorCurrentLimiter& fet_thermistor_; + OffboardThermistorCurrentLimiter& motor_thermistor_; Motor& motor_; - TrapezoidalTrajectory& trap_; + TrapezoidalTrajectory& trap_traj_; + Endstop& min_endstop_; + Endstop& max_endstop_; + + // List of current_limiters and thermistors to + // provide easy iteration. + std::array current_limiters_; + std::array thermistors_; osThreadId thread_id_; + const uint32_t stack_size_ = 2048; // Bytes volatile bool thread_id_valid_ = false; // variables exposed on protocol - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; bool step_dir_active_ = false; // auto enabled after calibration, based on config.enable_step_dir // updated from config in constructor, and on protocol hook @@ -213,80 +226,17 @@ public: GPIO_TypeDef* dir_port_; uint16_t dir_pin_; - State_t requested_state_ = AXIS_STATE_STARTUP_SEQUENCE; - State_t task_chain_[10] = { AXIS_STATE_UNDEFINED }; - State_t& current_state_ = task_chain_[0]; + AxisState requested_state_ = AXIS_STATE_STARTUP_SEQUENCE; + std::array task_chain_ = { AXIS_STATE_UNDEFINED }; + AxisState& current_state_ = task_chain_.front(); uint32_t loop_counter_ = 0; - LockinState_t lockin_state_ = LOCKIN_STATE_INACTIVE; + LockinState lockin_state_ = LOCKIN_STATE_INACTIVE; + Homing_t homing_; + uint32_t last_heartbeat_ = 0; // watchdog - uint32_t watchdog_reset_value_ = 0; //computed from config_.watchdog_timeout in update_watchdog_settings() uint32_t watchdog_current_value_= 0; - - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_ro_property("step_dir_active", &step_dir_active_), - make_protocol_ro_property("current_state", ¤t_state_), - make_protocol_property("requested_state", &requested_state_), - make_protocol_ro_property("loop_counter", &loop_counter_), - make_protocol_ro_property("lockin_state", &lockin_state_), - make_protocol_object("config", - make_protocol_property("startup_motor_calibration", &config_.startup_motor_calibration), - make_protocol_property("startup_encoder_index_search", &config_.startup_encoder_index_search), - make_protocol_property("startup_encoder_offset_calibration", &config_.startup_encoder_offset_calibration), - make_protocol_property("startup_closed_loop_control", &config_.startup_closed_loop_control), - make_protocol_property("startup_sensorless_control", &config_.startup_sensorless_control), - make_protocol_property("enable_step_dir", &config_.enable_step_dir), - make_protocol_property("counts_per_step", &config_.counts_per_step), - make_protocol_property("watchdog_timeout", &config_.watchdog_timeout, - [](void* ctx) { static_cast(ctx)->update_watchdog_settings(); }, this), - make_protocol_property("step_gpio_pin", &config_.step_gpio_pin, - [](void* ctx) { static_cast(ctx)->decode_step_dir_pins(); }, this), - make_protocol_property("dir_gpio_pin", &config_.dir_gpio_pin, - [](void* ctx) { static_cast(ctx)->decode_step_dir_pins(); }, this), - make_protocol_object("calibration_lockin", - make_protocol_property("current", &config_.calibration_lockin.current), - make_protocol_property("ramp_time", &config_.calibration_lockin.ramp_time), - make_protocol_property("ramp_distance", &config_.calibration_lockin.ramp_distance), - make_protocol_property("accel", &config_.calibration_lockin.accel), - make_protocol_property("vel", &config_.calibration_lockin.vel) - ), - make_protocol_object("sensorless_ramp", - make_protocol_property("current", &config_.sensorless_ramp.current), - make_protocol_property("ramp_time", &config_.sensorless_ramp.ramp_time), - make_protocol_property("ramp_distance", &config_.sensorless_ramp.ramp_distance), - make_protocol_property("accel", &config_.sensorless_ramp.accel), - make_protocol_property("vel", &config_.sensorless_ramp.vel), - make_protocol_property("finish_distance", &config_.sensorless_ramp.finish_distance), - make_protocol_property("finish_on_vel", &config_.sensorless_ramp.finish_on_vel), - make_protocol_property("finish_on_distance", &config_.sensorless_ramp.finish_on_distance), - make_protocol_property("finish_on_enc_idx", &config_.sensorless_ramp.finish_on_enc_idx) - ), - make_protocol_object("general_lockin", - make_protocol_property("current", &config_.lockin.current), - make_protocol_property("ramp_time", &config_.lockin.ramp_time), - make_protocol_property("ramp_distance", &config_.lockin.ramp_distance), - make_protocol_property("accel", &config_.lockin.accel), - make_protocol_property("vel", &config_.lockin.vel), - make_protocol_property("finish_distance", &config_.lockin.finish_distance), - make_protocol_property("finish_on_vel", &config_.lockin.finish_on_vel), - make_protocol_property("finish_on_distance", &config_.lockin.finish_on_distance), - make_protocol_property("finish_on_enc_idx", &config_.lockin.finish_on_enc_idx) - ) - ), - make_protocol_object("motor", motor_.make_protocol_definitions()), - make_protocol_object("controller", controller_.make_protocol_definitions()), - make_protocol_object("encoder", encoder_.make_protocol_definitions()), - make_protocol_object("sensorless_estimator", sensorless_estimator_.make_protocol_definitions()), - make_protocol_object("trap_traj", trap_.make_protocol_definitions()), - make_protocol_function("watchdog_feed", *this, &Axis::watchdog_feed) - ); - } }; -DEFINE_ENUM_FLAG_OPERATORS(Axis::Error_t) - #endif /* __AXIS_HPP */ diff --git a/Firmware/MotorControl/board_config_v3.h b/Firmware/MotorControl/board_config_v3.h index 58a8b3e2..64cbdc78 100644 --- a/Firmware/MotorControl/board_config_v3.h +++ b/Firmware/MotorControl/board_config_v3.h @@ -36,13 +36,18 @@ typedef struct { uint16_t hallB_pin; GPIO_TypeDef* hallC_port; uint16_t hallC_pin; + SPI_HandleTypeDef* spi; } EncoderHardwareConfig_t; typedef struct { TIM_HandleTypeDef* timer; uint16_t control_deadline; float shunt_conductance; - size_t inverter_thermistor_adc_ch; } MotorHardwareConfig_t; +typedef struct { + const float* const coeffs; + size_t num_coeffs; + size_t adc_ch; +} ThermistorHardwareConfig_t; typedef struct { SPI_HandleTypeDef* spi; GPIO_TypeDef* enable_port; @@ -56,18 +61,17 @@ typedef struct { AxisHardwareConfig_t axis_config; EncoderHardwareConfig_t encoder_config; MotorHardwareConfig_t motor_config; + ThermistorHardwareConfig_t thermistor_config; GateDriverHardwareConfig_t gate_driver_config; } BoardHardwareConfig_t; extern const BoardHardwareConfig_t hw_configs[2]; -extern const float thermistor_poly_coeffs[]; -extern const size_t thermistor_num_coeffs; //TODO stick this in a C file #ifdef __MAIN_CPP__ -const float thermistor_poly_coeffs[] = +const float fet_thermistor_poly_coeffs[] = {363.93910201f, -462.15369634f, 307.55129571f, -27.72569531f}; -const size_t thermistor_num_coeffs = sizeof(thermistor_poly_coeffs)/sizeof(thermistor_poly_coeffs[1]); +const size_t fet_thermistor_num_coeffs = sizeof(fet_thermistor_poly_coeffs)/sizeof(fet_thermistor_poly_coeffs[1]); const BoardHardwareConfig_t hw_configs[2] = { { //M0 @@ -86,12 +90,17 @@ const BoardHardwareConfig_t hw_configs[2] = { { .hallB_pin = M0_ENC_B_Pin, .hallC_port = M0_ENC_Z_GPIO_Port, .hallC_pin = M0_ENC_Z_Pin, + .spi = &hspi3, }, .motor_config = { .timer = &htim1, .control_deadline = TIM_1_8_PERIOD_CLOCKS, .shunt_conductance = 1.0f / SHUNT_RESISTANCE, //[S] - .inverter_thermistor_adc_ch = 15, + }, + .thermistor_config = { + .coeffs = &fet_thermistor_poly_coeffs[0], + .num_coeffs = fet_thermistor_num_coeffs, + .adc_ch = 15, }, .gate_driver_config = { .spi = &hspi3, @@ -125,15 +134,20 @@ const BoardHardwareConfig_t hw_configs[2] = { { .hallB_pin = M1_ENC_B_Pin, .hallC_port = M1_ENC_Z_GPIO_Port, .hallC_pin = M1_ENC_Z_Pin, + .spi = &hspi3, }, .motor_config = { .timer = &htim8, .control_deadline = (3 * TIM_1_8_PERIOD_CLOCKS) / 2, .shunt_conductance = 1.0f / SHUNT_RESISTANCE, //[S] + }, + .thermistor_config = { + .coeffs = &fet_thermistor_poly_coeffs[0], + .num_coeffs = fet_thermistor_num_coeffs, #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 - .inverter_thermistor_adc_ch = 4, + .adc_ch = 4, #else - .inverter_thermistor_adc_ch = 1, + .adc_ch = 1, #endif }, .gate_driver_config = { diff --git a/Firmware/MotorControl/controller.cpp b/Firmware/MotorControl/controller.cpp index d295246c..ef87376a 100644 --- a/Firmware/MotorControl/controller.cpp +++ b/Firmware/MotorControl/controller.cpp @@ -1,19 +1,23 @@ #include "odrive_main.h" +#include +#include Controller::Controller(Config_t& config) : config_(config) -{} +{ + update_filter_gains(); +} void Controller::reset() { pos_setpoint_ = 0.0f; vel_setpoint_ = 0.0f; - vel_integrator_current_ = 0.0f; - current_setpoint_ = 0.0f; + vel_integrator_torque_ = 0.0f; + torque_setpoint_ = 0.0f; } -void Controller::set_error(Error_t error) { +void Controller::set_error(Error error) { error_ |= error; axis_->error_ |= Axis::ERROR_CONTROLLER_FAILED; } @@ -22,58 +26,49 @@ void Controller::set_error(Error_t error) { // Command Handling //-------------------------------- -void Controller::set_pos_setpoint(float pos_setpoint, float vel_feed_forward, float current_feed_forward) { - pos_setpoint_ = pos_setpoint; - vel_setpoint_ = vel_feed_forward; - current_setpoint_ = current_feed_forward; - config_.control_mode = CTRL_MODE_POSITION_CONTROL; -#ifdef DEBUG_PRINT - printf("POSITION_CONTROL %6.0f %3.3f %3.3f\n", pos_setpoint, vel_setpoint_, current_setpoint_); -#endif -} -void Controller::set_vel_setpoint(float vel_setpoint, float current_feed_forward) { - vel_setpoint_ = vel_setpoint; - current_setpoint_ = current_feed_forward; - config_.control_mode = CTRL_MODE_VELOCITY_CONTROL; -#ifdef DEBUG_PRINT - printf("VELOCITY_CONTROL %3.3f %3.3f\n", vel_setpoint_, motor->current_setpoint_); -#endif -} - -void Controller::set_current_setpoint(float current_setpoint) { - current_setpoint_ = current_setpoint; - config_.control_mode = CTRL_MODE_CURRENT_CONTROL; -#ifdef DEBUG_PRINT - printf("CURRENT_CONTROL %3.3f\n", current_setpoint_); -#endif +bool Controller::select_encoder(size_t encoder_num) { + if (encoder_num < AXIS_COUNT) { + Axis* ax = axes[encoder_num]; + pos_estimate_circular_src_ = &ax->encoder_.pos_circular_; + pos_wrap_src_ = &config_.circular_setpoint_range; + pos_estimate_linear_src_ = &ax->encoder_.pos_estimate_; + pos_estimate_valid_src_ = &ax->encoder_.pos_estimate_valid_; + vel_estimate_src_ = &ax->encoder_.vel_estimate_; + vel_estimate_valid_src_ = &ax->encoder_.vel_estimate_valid_; + return true; + } else { + return set_error(Controller::ERROR_INVALID_LOAD_ENCODER), false; + } } void Controller::move_to_pos(float goal_point) { - axis_->trap_.planTrapezoidal(goal_point, pos_setpoint_, vel_setpoint_, - axis_->trap_.config_.vel_limit, - axis_->trap_.config_.accel_limit, - axis_->trap_.config_.decel_limit); - traj_start_loop_count_ = axis_->loop_counter_; - config_.control_mode = CTRL_MODE_TRAJECTORY_CONTROL; - goal_point_ = goal_point; + axis_->trap_traj_.planTrapezoidal(goal_point, pos_setpoint_, vel_setpoint_, + axis_->trap_traj_.config_.vel_limit, + axis_->trap_traj_.config_.accel_limit, + axis_->trap_traj_.config_.decel_limit); + axis_->trap_traj_.t_ = 0.0f; + trajectory_done_ = false; } -void Controller::move_incremental(float displacement, bool from_goal_point = true){ - if(from_goal_point){ - move_to_pos(goal_point_ + displacement); +void Controller::move_incremental(float displacement, bool from_input_pos = true){ + if(from_input_pos){ + input_pos_ += displacement; } else{ - move_to_pos(pos_setpoint_ + displacement); + input_pos_ = pos_setpoint_ + displacement; } + + input_pos_updated(); } void Controller::start_anticogging_calibration() { // Ensure the cogging map was correctly allocated earlier and that the motor is capable of calibrating - if (anticogging_.cogging_map != NULL && axis_->error_ == Axis::ERROR_NONE) { - anticogging_.calib_anticogging = true; + if (axis_->error_ == Axis::ERROR_NONE) { + config_.anticogging.calib_anticogging = true; } } + /* * This anti-cogging implementation iterates through each encoder position, * waits for zero velocity & position error, @@ -82,140 +77,270 @@ void Controller::start_anticogging_calibration() { * This holding current is added as a feedforward term in the control loop. */ bool Controller::anticogging_calibration(float pos_estimate, float vel_estimate) { - if (anticogging_.calib_anticogging && anticogging_.cogging_map != NULL) { - float pos_err = anticogging_.index - pos_estimate; - if (fabsf(pos_err) <= anticogging_.calib_pos_threshold && - fabsf(vel_estimate) < anticogging_.calib_vel_threshold) { - anticogging_.cogging_map[anticogging_.index++] = vel_integrator_current_; - } - if (anticogging_.index < axis_->encoder_.config_.cpr) { // TODO: remove the dependency on encoder CPR - set_pos_setpoint(anticogging_.index, 0.0f, 0.0f); - return false; - } else { - anticogging_.index = 0; - set_pos_setpoint(0.0f, 0.0f, 0.0f); // Send the motor home - anticogging_.use_anticogging = true; // We're good to go, enable anti-cogging - anticogging_.calib_anticogging = false; - return true; - } + float pos_err = input_pos_ - pos_estimate; + if (std::abs(pos_err) <= config_.anticogging.calib_pos_threshold / (float)axis_->encoder_.config_.cpr && + std::abs(vel_estimate) < config_.anticogging.calib_vel_threshold / (float)axis_->encoder_.config_.cpr) { + config_.anticogging.cogging_map[std::clamp(config_.anticogging.index++, 0, 3600)] = vel_integrator_torque_; + } + if (config_.anticogging.index < 3600) { + config_.control_mode = CONTROL_MODE_POSITION_CONTROL; + input_pos_ = config_.anticogging.index * axis_->encoder_.getCoggingRatio(); + input_vel_ = 0.0f; + input_torque_ = 0.0f; + input_pos_updated(); + return false; + } else { + config_.anticogging.index = 0; + config_.control_mode = CONTROL_MODE_POSITION_CONTROL; + input_pos_ = 0.0f; // Send the motor home + input_vel_ = 0.0f; + input_torque_ = 0.0f; + input_pos_updated(); + anticogging_valid_ = true; + config_.anticogging.calib_anticogging = false; + return true; } - return false; } -bool Controller::update(float pos_estimate, float vel_estimate, float* current_setpoint_output) { - // Only runs if anticogging_.calib_anticogging is true; non-blocking - anticogging_calibration(pos_estimate, vel_estimate); - float anticogging_pos = pos_estimate; +void Controller::update_filter_gains() { + float bandwidth = std::min(config_.input_filter_bandwidth, 0.25f * current_meas_hz); + input_filter_ki_ = 2.0f * bandwidth; // basic conversion to discrete time + input_filter_kp_ = 0.25f * (input_filter_ki_ * input_filter_ki_); // Critically damped +} - // Trajectory control - if (config_.control_mode == CTRL_MODE_TRAJECTORY_CONTROL) { - // Note: uint32_t loop count delta is OK across overflow - // Beware of negative deltas, as they will not be well behaved due to uint! - float t = (axis_->loop_counter_ - traj_start_loop_count_) * current_meas_period; - if (t > axis_->trap_.Tf_) { - // Drop into position control mode when done to avoid problems on loop counter delta overflow - config_.control_mode = CTRL_MODE_POSITION_CONTROL; - // pos_setpoint already set by trajectory - vel_setpoint_ = 0.0f; - current_setpoint_ = 0.0f; - } else { - TrapezoidalTrajectory::Step_t traj_step = axis_->trap_.eval(t); - pos_setpoint_ = traj_step.Y; - vel_setpoint_ = traj_step.Yd; - current_setpoint_ = traj_step.Ydd * axis_->trap_.config_.A_per_css; +static float limitVel(const float vel_limit, const float vel_estimate, const float vel_gain, const float torque) { + float Tmax = (vel_limit - vel_estimate) * vel_gain; + float Tmin = (-vel_limit - vel_estimate) * vel_gain; + return std::clamp(torque, Tmin, Tmax); +} + +bool Controller::update(float* torque_setpoint_output) { + float* pos_estimate_linear = (pos_estimate_valid_src_ && *pos_estimate_valid_src_) + ? pos_estimate_linear_src_ : nullptr; + float* pos_estimate_circular = (pos_estimate_valid_src_ && *pos_estimate_valid_src_) + ? pos_estimate_circular_src_ : nullptr; + float* vel_estimate_src = (vel_estimate_valid_src_ && *vel_estimate_valid_src_) + ? vel_estimate_src_ : nullptr; + + // Calib_anticogging is only true when calibration is occurring, so we can't block anticogging_pos + float anticogging_pos = axis_->encoder_.pos_estimate_ / axis_->encoder_.getCoggingRatio(); + if (config_.anticogging.calib_anticogging) { + if (!axis_->encoder_.pos_estimate_valid_ || !axis_->encoder_.vel_estimate_valid_) { + set_error(ERROR_INVALID_ESTIMATE); + return false; } - anticogging_pos = pos_setpoint_; // FF the position setpoint instead of the pos_estimate + // non-blocking + anticogging_calibration(axis_->encoder_.pos_estimate_, axis_->encoder_.vel_estimate_); } - // Ramp rate limited velocity setpoint - if (config_.control_mode == CTRL_MODE_VELOCITY_CONTROL && vel_ramp_enable_) { - float max_step_size = current_meas_period * config_.vel_ramp_rate; - float full_step = vel_ramp_target_ - vel_setpoint_; - float step; - if (fabsf(full_step) > max_step_size) { - step = std::copysignf(max_step_size, full_step); - } else { - step = full_step; + // TODO also enable circular deltas for 2nd order filter, etc. + if (config_.circular_setpoints) { + // Keep pos setpoint from drifting + input_pos_ = fmodf_pos(input_pos_, config_.circular_setpoint_range); + } + + // Update inputs + switch (config_.input_mode) { + case INPUT_MODE_INACTIVE: { + // do nothing + } break; + case INPUT_MODE_PASSTHROUGH: { + pos_setpoint_ = input_pos_; + vel_setpoint_ = input_vel_; + torque_setpoint_ = input_torque_; + } break; + case INPUT_MODE_VEL_RAMP: { + float max_step_size = std::abs(current_meas_period * config_.vel_ramp_rate); + float full_step = input_vel_ - vel_setpoint_; + float step = std::clamp(full_step, -max_step_size, max_step_size); + + vel_setpoint_ += step; + torque_setpoint_ = (step / current_meas_period) * config_.inertia; + } break; + case INPUT_MODE_TORQUE_RAMP: { + float max_step_size = std::abs(current_meas_period * config_.torque_ramp_rate); + float full_step = input_torque_ - torque_setpoint_; + float step = std::clamp(full_step, -max_step_size, max_step_size); + + torque_setpoint_ += step; + } break; + case INPUT_MODE_POS_FILTER: { + // 2nd order pos tracking filter + float delta_pos = input_pos_ - pos_setpoint_; // Pos error + float delta_vel = input_vel_ - vel_setpoint_; // Vel error + float accel = input_filter_kp_*delta_pos + input_filter_ki_*delta_vel; // Feedback + torque_setpoint_ = accel * config_.inertia; // Accel + vel_setpoint_ += current_meas_period * accel; // delta vel + pos_setpoint_ += current_meas_period * vel_setpoint_; // Delta pos + } break; + case INPUT_MODE_MIRROR: { + if (config_.axis_to_mirror < AXIS_COUNT) { + pos_setpoint_ = axes[config_.axis_to_mirror]->encoder_.pos_estimate_ * config_.mirror_ratio; + vel_setpoint_ = axes[config_.axis_to_mirror]->encoder_.vel_estimate_ * config_.mirror_ratio; + } else { + set_error(ERROR_INVALID_MIRROR_AXIS); + return false; + } + } break; + // case INPUT_MODE_MIX_CHANNELS: { + // // NOT YET IMPLEMENTED + // } break; + case INPUT_MODE_TRAP_TRAJ: { + if(input_pos_updated_){ + move_to_pos(input_pos_); + input_pos_updated_ = false; + } + // Avoid updating uninitialized trajectory + if (trajectory_done_) + break; + + if (axis_->trap_traj_.t_ > axis_->trap_traj_.Tf_) { + // Drop into position control mode when done to avoid problems on loop counter delta overflow + config_.control_mode = CONTROL_MODE_POSITION_CONTROL; + pos_setpoint_ = input_pos_; + vel_setpoint_ = 0.0f; + torque_setpoint_ = 0.0f; + trajectory_done_ = true; + } else { + TrapezoidalTrajectory::Step_t traj_step = axis_->trap_traj_.eval(axis_->trap_traj_.t_); + pos_setpoint_ = traj_step.Y; + vel_setpoint_ = traj_step.Yd; + torque_setpoint_ = traj_step.Ydd * config_.inertia; + axis_->trap_traj_.t_ += current_meas_period; + } + anticogging_pos = pos_setpoint_; // FF the position setpoint instead of the pos_estimate + } break; + default: { + set_error(ERROR_INVALID_INPUT_MODE); + return false; } - vel_setpoint_ += step; + } // Position control // TODO Decide if we want to use encoder or pll position here + float gain_scheduling_multiplier = 1.0f; float vel_des = vel_setpoint_; - if (config_.control_mode >= CTRL_MODE_POSITION_CONTROL) { + if (config_.control_mode >= CONTROL_MODE_POSITION_CONTROL) { float pos_err; - if (config_.setpoints_in_cpr) { - // TODO this breaks the semantics that estimates come in on the arguments. - // It's probably better to call a get_estimate that will arbitrate (enc vs sensorless) instead. - float cpr = (float)(axis_->encoder_.config_.cpr); + + if (config_.circular_setpoints) { + if(!pos_estimate_circular) { + set_error(ERROR_INVALID_ESTIMATE); + return false; + } // Keep pos setpoint from drifting - pos_setpoint_ = fmodf_pos(pos_setpoint_, cpr); + pos_setpoint_ = fmodf_pos(pos_setpoint_, *pos_wrap_src_); // Circular delta - pos_err = pos_setpoint_ - axis_->encoder_.pos_cpr_; - pos_err = wrap_pm(pos_err, 0.5f * cpr); + pos_err = pos_setpoint_ - *pos_estimate_circular; + pos_err = wrap_pm(pos_err, 0.5f * *pos_wrap_src_); } else { - pos_err = pos_setpoint_ - pos_estimate; + if(!pos_estimate_linear) { + set_error(ERROR_INVALID_ESTIMATE); + return false; + } + pos_err = pos_setpoint_ - *pos_estimate_linear; } + vel_des += config_.pos_gain * pos_err; + // V-shaped gain shedule based on position error + float abs_pos_err = std::abs(pos_err); + if (config_.enable_gain_scheduling && abs_pos_err <= config_.gain_scheduling_width) { + gain_scheduling_multiplier = abs_pos_err / config_.gain_scheduling_width; + } } // Velocity limiting float vel_lim = config_.vel_limit; - if (vel_des > vel_lim) vel_des = vel_lim; - if (vel_des < -vel_lim) vel_des = -vel_lim; + if (config_.enable_vel_limit) { + vel_des = std::clamp(vel_des, -vel_lim, vel_lim); + } // Check for overspeed fault (done in this module (controller) for cohesion with vel_lim) - if (config_.vel_limit_tolerance > 0.0f) { // 0.0f to disable - if (fabsf(vel_estimate) > config_.vel_limit_tolerance * vel_lim) { + if (config_.enable_overspeed_error) { // 0.0f to disable + if (!vel_estimate_src) { + set_error(ERROR_INVALID_ESTIMATE); + return false; + } + if (std::abs(*vel_estimate_src) > config_.vel_limit_tolerance * vel_lim) { set_error(ERROR_OVERSPEED); return false; } } + // TODO: Change to controller working in torque units + // Torque per amp gain scheduling (ACIM) + float vel_gain = config_.vel_gain; + float vel_integrator_gain = config_.vel_integrator_gain; + if (axis_->motor_.config_.motor_type == Motor::MOTOR_TYPE_ACIM) { + float effective_flux = axis_->motor_.current_control_.acim_rotor_flux; + float minflux = axis_->motor_.config_.acim_gain_min_flux; + if (fabsf(effective_flux) < minflux) + effective_flux = std::copysignf(minflux, effective_flux); + vel_gain /= effective_flux; + vel_integrator_gain /= effective_flux; + // TODO: also scale the integral value which is also changing units. + // (or again just do control in torque units) + } + // Velocity control - float Iq = current_setpoint_; + float torque = torque_setpoint_; // Anti-cogging is enabled after calibration // We get the current position and apply a current feed-forward // ensuring that we handle negative encoder positions properly (-1 == motor->encoder.encoder_cpr - 1) - if (anticogging_.use_anticogging) { - Iq += anticogging_.cogging_map[mod(static_cast(anticogging_pos), axis_->encoder_.config_.cpr)]; + if (anticogging_valid_ && config_.anticogging.anticogging_enabled) { + torque += config_.anticogging.cogging_map[std::clamp(mod((int)anticogging_pos, 3600), 0, 3600)]; } - float v_err = vel_des - vel_estimate; - if (config_.control_mode >= CTRL_MODE_VELOCITY_CONTROL) { - Iq += config_.vel_gain * v_err; + float v_err = 0.0f; + if (config_.control_mode >= CONTROL_MODE_VELOCITY_CONTROL) { + if (!vel_estimate_src) { + set_error(ERROR_INVALID_ESTIMATE); + return false; + } + + v_err = vel_des - *vel_estimate_src; + torque += (vel_gain * gain_scheduling_multiplier) * v_err; + + // Velocity integral action before limiting + torque += vel_integrator_torque_; } - // Velocity integral action before limiting - Iq += vel_integrator_current_; + // Velocity limiting in current mode + if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL && config_.enable_current_mode_vel_limit) { + if (!vel_estimate_src) { + set_error(ERROR_INVALID_ESTIMATE); + return false; + } + torque = limitVel(config_.vel_limit, *vel_estimate_src, vel_gain, torque); + } - // Current limiting + // Torque limiting bool limited = false; - float Ilim = axis_->motor_.effective_current_lim(); - if (Iq > Ilim) { + float Tlim = axis_->motor_.max_available_torque(); + if (torque > Tlim) { limited = true; - Iq = Ilim; + torque = Tlim; } - if (Iq < -Ilim) { + if (torque < -Tlim) { limited = true; - Iq = -Ilim; + torque = -Tlim; } // Velocity integrator (behaviour dependent on limiting) - if (config_.control_mode < CTRL_MODE_VELOCITY_CONTROL) { + if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL) { // reset integral if not in use - vel_integrator_current_ = 0.0f; + vel_integrator_torque_ = 0.0f; } else { if (limited) { // TODO make decayfactor configurable - vel_integrator_current_ *= 0.99f; + vel_integrator_torque_ *= 0.99f; } else { - vel_integrator_current_ += (config_.vel_integrator_gain * current_meas_period) * v_err; + vel_integrator_torque_ += ((vel_integrator_gain * gain_scheduling_multiplier) * current_meas_period) * v_err; } } - if (current_setpoint_output) *current_setpoint_output = Iq; + if (torque_setpoint_output) *torque_setpoint_output = torque; return true; } diff --git a/Firmware/MotorControl/controller.hpp b/Firmware/MotorControl/controller.hpp index 020f34d0..8022e946 100644 --- a/Firmware/MotorControl/controller.hpp +++ b/Firmware/MotorControl/controller.hpp @@ -5,42 +5,59 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Controller { +class Controller : public ODriveIntf::ControllerIntf { public: - enum Error_t { - ERROR_NONE = 0, - ERROR_OVERSPEED = 0x01, - }; - - // Note: these should be sorted from lowest level of control to - // highest level of control, to allow "<" style comparisons. - enum ControlMode_t{ - CTRL_MODE_VOLTAGE_CONTROL = 0, - CTRL_MODE_CURRENT_CONTROL = 1, - CTRL_MODE_VELOCITY_CONTROL = 2, - CTRL_MODE_POSITION_CONTROL = 3, - CTRL_MODE_TRAJECTORY_CONTROL = 4 - }; + typedef struct { + uint32_t index = 0; + float cogging_map[3600]; + bool pre_calibrated = false; + bool calib_anticogging = false; + float calib_pos_threshold = 1.0f; + float calib_vel_threshold = 1.0f; + float cogging_ratio = 1.0f; + bool anticogging_enabled = true; + } Anticogging_t; struct Config_t { - ControlMode_t control_mode = CTRL_MODE_POSITION_CONTROL; //see: Motor_control_mode_t - float pos_gain = 20.0f; // [(counts/s) / counts] - float vel_gain = 5.0f / 10000.0f; // [A/(counts/s)] - // float vel_gain = 5.0f / 200.0f, // [A/(rad/s)] - float vel_integrator_gain = 10.0f / 10000.0f; // [A/(counts/s * s)] - float vel_limit = 20000.0f; // [counts/s] - float vel_limit_tolerance = 1.2f; // ratio to vel_lim. 0.0f to disable - float vel_ramp_rate = 10000.0f; // [(counts/s) / s] - bool setpoints_in_cpr = false; + ControlMode control_mode = CONTROL_MODE_POSITION_CONTROL; //see: ControlMode_t + InputMode input_mode = INPUT_MODE_PASSTHROUGH; //see: InputMode_t + float pos_gain = 20.0f; // [(turn/s) / turn] + float vel_gain = 1.0f / 6.0f; // [Nm/(turn/s)] + // float vel_gain = 0.2f / 200.0f, // [Nm/(rad/s)] + float vel_integrator_gain = 2.0f / 6.0f; // [Nm/(turn/s * s)] + float vel_limit = 2.0f; // [turn/s] Infinity to disable. + float vel_limit_tolerance = 1.2f; // ratio to vel_lim. Infinity to disable. + float vel_ramp_rate = 1.0f; // [(turn/s) / s] + float torque_ramp_rate = 0.01f; // Nm / sec + bool circular_setpoints = false; + float circular_setpoint_range = 1.0f; // Circular range when circular_setpoints is true. [turn] + float inertia = 0.0f; // [Nm/(turn/s^2)] + float input_filter_bandwidth = 2.0f; // [1/s] + float homing_speed = 0.25f; // [turn/s] + Anticogging_t anticogging; + float gain_scheduling_width = 10.0f; + bool enable_gain_scheduling = false; + bool enable_vel_limit = true; + bool enable_overspeed_error = true; + bool enable_current_mode_vel_limit = true; // enable velocity limit in current control mode (requires a valid velocity estimator) + uint8_t axis_to_mirror = -1; + float mirror_ratio = 1.0f; + uint8_t load_encoder_axis = -1; // default depends on Axis number and is set in load_configuration() + + // custom setters + Controller* parent; + void set_input_filter_bandwidth(float value) { input_filter_bandwidth = value; parent->update_filter_gains(); } }; explicit Controller(Config_t& config); void reset(); - void set_error(Error_t error); + void set_error(Error error); - void set_pos_setpoint(float pos_setpoint, float vel_feed_forward, float current_feed_forward); - void set_vel_setpoint(float vel_setpoint, float current_feed_forward); - void set_current_setpoint(float current_setpoint); + constexpr void input_pos_updated() { + input_pos_updated_ = true; + } + + bool select_encoder(size_t encoder_num); // Trajectory-Planned control void move_to_pos(float goal_point); @@ -50,81 +67,43 @@ public: void start_anticogging_calibration(); bool anticogging_calibration(float pos_estimate, float vel_estimate); - bool update(float pos_estimate, float vel_estimate, float* current_setpoint); + void update_filter_gains(); + bool update(float* torque_setpoint); Config_t& config_; Axis* axis_ = nullptr; // set by Axis constructor - // TODO: anticogging overhaul: - // - expose selected (all?) variables on protocol - // - make calibration user experience similar to motor & encoder calibration - // - use python tools to Fourier transform and write back the smoothed map or Fourier coefficients - // - make the calibration persistent + Error error_ = ERROR_NONE; - typedef struct { - int index; - float *cogging_map; - bool use_anticogging; - bool calib_anticogging; - float calib_pos_threshold; - float calib_vel_threshold; - } Anticogging_t; - Anticogging_t anticogging_ = { - .index = 0, - .cogging_map = nullptr, - .use_anticogging = false, - .calib_anticogging = false, - .calib_pos_threshold = 1.0f, - .calib_vel_threshold = 1.0f, - }; + float* pos_estimate_linear_src_ = nullptr; + float* pos_estimate_circular_src_ = nullptr; + bool* pos_estimate_valid_src_ = nullptr; + float* vel_estimate_src_ = nullptr; + bool* vel_estimate_valid_src_ = nullptr; + float* pos_wrap_src_ = nullptr; - Error_t error_ = ERROR_NONE; - // variables exposed on protocol - float pos_setpoint_ = 0.0f; - float vel_setpoint_ = 0.0f; + + float pos_setpoint_ = 0.0f; // [turns] + float vel_setpoint_ = 0.0f; // [turn/s] // float vel_setpoint = 800.0f; - float vel_integrator_current_ = 0.0f; // [A] - float current_setpoint_ = 0.0f; // [A] - float vel_ramp_target_ = 0.0f; - bool vel_ramp_enable_ = false; + float vel_integrator_torque_ = 0.0f; // [Nm] + float torque_setpoint_ = 0.0f; // [Nm] - uint32_t traj_start_loop_count_ = 0; + float input_pos_ = 0.0f; // [turns] + float input_vel_ = 0.0f; // [turn/s] + float input_torque_ = 0.0f; // [Nm] + float input_filter_kp_ = 0.0f; + float input_filter_ki_ = 0.0f; - float goal_point_ = 0.0f; + bool input_pos_updated_ = false; + + bool trajectory_done_ = true; + + bool anticogging_valid_ = false; + + // custom setters + void set_input_pos(float value) { input_pos_ = value; input_pos_updated(); } - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_property("pos_setpoint", &pos_setpoint_), - make_protocol_property("vel_setpoint", &vel_setpoint_), - make_protocol_property("vel_integrator_current", &vel_integrator_current_), - make_protocol_property("current_setpoint", ¤t_setpoint_), - make_protocol_property("vel_ramp_target", &vel_ramp_target_), - make_protocol_property("vel_ramp_enable", &vel_ramp_enable_), - make_protocol_object("config", - make_protocol_property("control_mode", &config_.control_mode), - make_protocol_property("pos_gain", &config_.pos_gain), - make_protocol_property("vel_gain", &config_.vel_gain), - make_protocol_property("vel_integrator_gain", &config_.vel_integrator_gain), - make_protocol_property("vel_limit", &config_.vel_limit), - make_protocol_property("vel_limit_tolerance", &config_.vel_limit_tolerance), - make_protocol_property("vel_ramp_rate", &config_.vel_ramp_rate), - make_protocol_property("setpoints_in_cpr", &config_.setpoints_in_cpr) - ), - make_protocol_function("set_pos_setpoint", *this, &Controller::set_pos_setpoint, - "pos_setpoint", "vel_feed_forward", "current_feed_forward"), - make_protocol_function("set_vel_setpoint", *this, &Controller::set_vel_setpoint, - "vel_setpoint", "current_feed_forward"), - make_protocol_function("set_current_setpoint", *this, &Controller::set_current_setpoint, - "current_setpoint"), - make_protocol_function("move_to_pos", *this, &Controller::move_to_pos, "pos_setpoint"), - make_protocol_function("move_incremental", *this, &Controller::move_incremental, "displacement", "from_goal_point"), - make_protocol_function("start_anticogging_calibration", *this, &Controller::start_anticogging_calibration) - ); - } }; -DEFINE_ENUM_FLAG_OPERATORS(Controller::Error_t) - #endif // __CONTROLLER_HPP diff --git a/Firmware/MotorControl/current_limiter.hpp b/Firmware/MotorControl/current_limiter.hpp new file mode 100644 index 00000000..4334f5c0 --- /dev/null +++ b/Firmware/MotorControl/current_limiter.hpp @@ -0,0 +1,14 @@ +#ifndef __CURRENT_LIMITER_HPP +#define __CURRENT_LIMITER_HPP + +#ifndef __ODRIVE_MAIN_H +#error "This file should not be included directly. Include odrive_main.h instead." +#endif + +class CurrentLimiter { +public: + virtual ~CurrentLimiter() = default; + virtual float get_current_limit(float base_current_lim) const = 0; +}; + +#endif // __CURRENT_LIMITER_HPP diff --git a/Firmware/MotorControl/encoder.cpp b/Firmware/MotorControl/encoder.cpp index 7dcdd57a..2c465988 100644 --- a/Firmware/MotorControl/encoder.cpp +++ b/Firmware/MotorControl/encoder.cpp @@ -3,14 +3,17 @@ Encoder::Encoder(const EncoderHardwareConfig_t& hw_config, - Config_t& config) : + Config_t& config, const Motor::Config_t& motor_config) : hw_config_(hw_config), config_(config) { update_pll_gains(); - if (config.pre_calibrated && (config.mode == Encoder::MODE_HALL || config.mode == Encoder::MODE_SINCOS)) { - is_ready_ = true; + if (config.pre_calibrated) { + if (config.mode == Encoder::MODE_HALL || config.mode == Encoder::MODE_SINCOS) + is_ready_ = true; + if (motor_config.motor_type == Motor::MOTOR_TYPE_ACIM) + is_ready_ = true; } } @@ -21,9 +24,20 @@ static void enc_index_cb_wrapper(void* ctx) { void Encoder::setup() { HAL_TIM_Encoder_Start(hw_config_.timer, TIM_CHANNEL_ALL); set_idx_subscribe(); + + mode_ = config_.mode; + if(mode_ & MODE_FLAG_ABS){ + abs_spi_cs_pin_init(); + abs_spi_init(); + if (axis_->controller_.config_.anticogging.pre_calibrated) { + axis_->controller_.anticogging_valid_ = true; + } + } } -void Encoder::set_error(Error_t error) { +void Encoder::set_error(Error error) { + vel_estimate_valid_ = false; + pos_estimate_valid_ = false; error_ |= error; axis_->error_ |= Axis::ERROR_ENCODER_FAILED; } @@ -46,6 +60,9 @@ void Encoder::enc_index_cb() { set_linear_count(0); // Avoid position control transient after search if (config_.pre_calibrated) { is_ready_ = true; + if(axis_->controller_.config_.anticogging.pre_calibrated){ + axis_->controller_.anticogging_valid_ = true; + } } else { // We can't use the update_offset facility in set_circular_count because // we also set the linear count before there is a chance to update. Therefore: @@ -79,9 +96,10 @@ void Encoder::update_pll_gains() { } void Encoder::check_pre_calibrated() { - if (!is_ready_) + // TODO: restoring config from python backup is fragile here (ACIM motor type must be set first) + if (!is_ready_ && axis_->motor_.config_.motor_type != Motor::MOTOR_TYPE_ACIM) config_.pre_calibrated = false; - if (config_.mode == MODE_INCREMENTAL && !index_found_) + if (mode_ == MODE_INCREMENTAL && !index_found_) config_.pre_calibrated = false; } @@ -92,7 +110,7 @@ void Encoder::set_linear_count(int32_t count) { // Update states shadow_count_ = count; - pos_estimate_ = (float)count; + pos_estimate_counts_ = (float)count; tim_cnt_sample_ = count; //Write hardware last @@ -114,7 +132,7 @@ void Encoder::set_circular_count(int32_t count, bool update_offset) { // Update states count_in_cpr_ = mod(count, config_.cpr); - pos_cpr_ = (float)count_in_cpr_; + pos_cpr_counts_ = (float)count_in_cpr_; cpu_exit_critical(prim); } @@ -133,11 +151,14 @@ bool Encoder::run_index_search() { bool Encoder::run_direction_find() { int32_t init_enc_val = shadow_count_; - bool orig_finish_on_distance = axis_->config_.calibration_lockin.finish_on_distance; - axis_->config_.calibration_lockin.finish_on_distance = true; axis_->motor_.config_.direction = 1; // Must test spin forwards for direction detect logic - bool status = axis_->run_lockin_spin(axis_->config_.calibration_lockin); - axis_->config_.calibration_lockin.finish_on_distance = orig_finish_on_distance; + + Axis::LockinConfig_t lockin_config = axis_->config_.calibration_lockin; + lockin_config.finish_distance = lockin_config.vel * 3.0f; // run for 3 seconds + lockin_config.finish_on_distance = true; + lockin_config.finish_on_enc_idx = false; + lockin_config.finish_on_vel = false; + bool status = axis_->run_lockin_spin(lockin_config); if (status) { // Check response and direction @@ -160,8 +181,8 @@ bool Encoder::run_direction_find() { // and the encoder state 0. // TODO: Do the scan with current, not voltage! bool Encoder::run_offset_calibration() { - static const float start_lock_duration = 1.0f; - static const int num_steps = (int)(config_.calib_scan_distance / config_.calib_scan_omega * (float)current_meas_hz); + const float start_lock_duration = 1.0f; + const int num_steps = (int)(config_.calib_scan_distance / config_.calib_scan_omega * (float)current_meas_hz); // Require index found if enabled if (config_.use_index && !index_found_) { @@ -186,7 +207,7 @@ bool Encoder::run_offset_calibration() { axis_->run_control_loop([&](){ if (!axis_->motor_.enqueue_voltage_timings(voltage_magnitude, 0.0f)) return false; // error set inside enqueue_voltage_timings - axis_->motor_.log_timing(Motor::TIMING_LOG_ENC_CALIB); + axis_->motor_.log_timing(TIMING_LOG_ENC_CALIB); return ++i < start_lock_duration * current_meas_hz; }); if (axis_->error_ != Axis::ERROR_NONE) @@ -197,13 +218,13 @@ bool Encoder::run_offset_calibration() { // scan forward i = 0; - axis_->run_control_loop([&](){ + axis_->run_control_loop([&]() { float phase = wrap_pm_pi(config_.calib_scan_distance * (float)i / (float)num_steps - config_.calib_scan_distance / 2.0f); float v_alpha = voltage_magnitude * our_arm_cos_f32(phase); float v_beta = voltage_magnitude * our_arm_sin_f32(phase); if (!axis_->motor_.enqueue_voltage_timings(v_alpha, v_beta)) return false; // error set inside enqueue_voltage_timings - axis_->motor_.log_timing(Motor::TIMING_LOG_ENC_CALIB); + axis_->motor_.log_timing(TIMING_LOG_ENC_CALIB); encvaluesum += shadow_count_; @@ -229,22 +250,21 @@ bool Encoder::run_offset_calibration() { // Check CPR float elec_rad_per_enc = axis_->motor_.config_.pole_pairs * 2 * M_PI * (1.0f / (float)(config_.cpr)); float expected_encoder_delta = config_.calib_scan_distance / elec_rad_per_enc; - calib_scan_response_ = fabsf(shadow_count_-init_enc_val); - if(fabsf(calib_scan_response_ - expected_encoder_delta)/expected_encoder_delta > config_.calib_range) - { - set_error(ERROR_CPR_OUT_OF_RANGE); + calib_scan_response_ = std::abs(shadow_count_ - init_enc_val); + if (std::abs(calib_scan_response_ - expected_encoder_delta) / expected_encoder_delta > config_.calib_range) { + set_error(ERROR_CPR_POLEPAIRS_MISMATCH); return false; } // scan backwards i = 0; - axis_->run_control_loop([&](){ + axis_->run_control_loop([&]() { float phase = wrap_pm_pi(-config_.calib_scan_distance * (float)i / (float)num_steps + config_.calib_scan_distance / 2.0f); float v_alpha = voltage_magnitude * our_arm_cos_f32(phase); float v_beta = voltage_magnitude * our_arm_sin_f32(phase); if (!axis_->motor_.enqueue_voltage_timings(v_alpha, v_beta)) return false; // error set inside enqueue_voltage_timings - axis_->motor_.log_timing(Motor::TIMING_LOG_ENC_CALIB); + axis_->motor_.log_timing(TIMING_LOG_ENC_CALIB); encvaluesum += shadow_count_; @@ -255,7 +275,7 @@ bool Encoder::run_offset_calibration() { config_.offset = encvaluesum / (num_steps * 2); int32_t residual = encvaluesum - ((int64_t)config_.offset * (int64_t)(num_steps * 2)); - config_.offset_float = (float)residual / (float)(num_steps * 2) + 0.5f; // add 0.5 to center-align state to phase + config_.offset_float = (float)residual / (float)(num_steps * 2) + 0.5f; // add 0.5 to center-align state to phase is_ready_ = true; return true; @@ -274,7 +294,7 @@ static bool decode_hall(uint8_t hall_state, int32_t* hall_cnt) { } void Encoder::sample_now() { - switch (config_.mode) { + switch (mode_) { case MODE_INCREMENTAL: { tim_cnt_sample_ = (int16_t)hw_config_.timer->Instance->CNT; } break; @@ -284,8 +304,17 @@ void Encoder::sample_now() { } break; case MODE_SINCOS: { - sincos_sample_s_ = (get_adc_voltage(GPIO_3_GPIO_Port, GPIO_3_Pin) / 3.3f) - 0.5f; - sincos_sample_c_ = (get_adc_voltage(GPIO_4_GPIO_Port, GPIO_4_Pin) / 3.3f) - 0.5f; + sincos_sample_s_ = (get_adc_voltage(get_gpio_port_by_pin(config_.sincos_gpio_pin_sin), get_gpio_pin_by_pin(config_.sincos_gpio_pin_sin)) / 3.3f) - 0.5f; + sincos_sample_c_ = (get_adc_voltage(get_gpio_port_by_pin(config_.sincos_gpio_pin_cos), get_gpio_pin_by_pin(config_.sincos_gpio_pin_cos)) / 3.3f) - 0.5f; + } break; + + case MODE_SPI_ABS_AMS: + case MODE_SPI_ABS_CUI: + case MODE_SPI_ABS_AEAT: + case MODE_SPI_ABS_RLS: + { + axis_->motor_.log_timing(TIMING_LOG_SAMPLE_NOW); + // Do nothing } break; default: { @@ -294,10 +323,126 @@ void Encoder::sample_now() { } } +bool Encoder::abs_spi_init(){ + if ((mode_ & MODE_FLAG_ABS) == 0x0) + return false; + + SPI_HandleTypeDef * spi = hw_config_.spi; + spi->Init.Mode = SPI_MODE_MASTER; + spi->Init.Direction = SPI_DIRECTION_2LINES; + spi->Init.DataSize = SPI_DATASIZE_16BIT; + spi->Init.CLKPolarity = SPI_POLARITY_LOW; + spi->Init.CLKPhase = SPI_PHASE_2EDGE; + spi->Init.NSS = SPI_NSS_SOFT; + spi->Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; + spi->Init.FirstBit = SPI_FIRSTBIT_MSB; + spi->Init.TIMode = SPI_TIMODE_DISABLE; + spi->Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; + spi->Init.CRCPolynomial = 10; + if (mode_ == MODE_SPI_ABS_AEAT) { + spi->Init.CLKPolarity = SPI_POLARITY_HIGH; + } + HAL_SPI_DeInit(spi); + HAL_SPI_Init(spi); + return true; +} + +bool Encoder::abs_spi_start_transaction(){ + if (mode_ & MODE_FLAG_ABS){ + axis_->motor_.log_timing(TIMING_LOG_SPI_START); + if(hw_config_.spi->State != HAL_SPI_STATE_READY){ + set_error(ERROR_ABS_SPI_NOT_READY); + return false; + } + HAL_GPIO_WritePin(abs_spi_cs_port_, abs_spi_cs_pin_, GPIO_PIN_RESET); + HAL_SPI_TransmitReceive_DMA(hw_config_.spi, (uint8_t*)abs_spi_dma_tx_, (uint8_t*)abs_spi_dma_rx_, 1); + } + return true; +} + +uint8_t ams_parity(uint16_t v) { + v ^= v >> 8; + v ^= v >> 4; + v ^= v >> 2; + v ^= v >> 1; + return v & 1; +} + +uint8_t cui_parity(uint16_t v) { + v ^= v >> 8; + v ^= v >> 4; + v ^= v >> 2; + return ~v & 3; +} + +void Encoder::abs_spi_cb(){ + HAL_GPIO_WritePin(abs_spi_cs_port_, abs_spi_cs_pin_, GPIO_PIN_SET); + + axis_->motor_.log_timing(TIMING_LOG_SPI_END); + + uint16_t pos; + + switch (mode_) { + case MODE_SPI_ABS_AMS: { + uint16_t rawVal = abs_spi_dma_rx_[0]; + // check if parity is correct (even) and error flag clear + if (ams_parity(rawVal) || ((rawVal >> 14) & 1)) { + return; + } + pos = rawVal & 0x3fff; + } break; + + case MODE_SPI_ABS_CUI: { + uint16_t rawVal = abs_spi_dma_rx_[0]; + // check if parity is correct + if (cui_parity(rawVal)) { + return; + } + pos = rawVal & 0x3fff; + } break; + + case MODE_SPI_ABS_RLS: { + uint16_t rawVal = abs_spi_dma_rx_[0]; + pos = (rawVal >> 2) & 0x3fff; + } break; + + default: { + set_error(ERROR_UNSUPPORTED_ENCODER_MODE); + return; + } break; + } + + pos_abs_ = pos; + abs_spi_pos_updated_ = true; + if (config_.pre_calibrated) { + is_ready_ = true; + } +} + +void Encoder::abs_spi_cs_pin_init(){ + // Decode cs pin + abs_spi_cs_port_ = get_gpio_port_by_pin(config_.abs_spi_cs_gpio_pin); + abs_spi_cs_pin_ = get_gpio_pin_by_pin(config_.abs_spi_cs_gpio_pin); + + // Init cs pin + HAL_GPIO_DeInit(abs_spi_cs_port_, abs_spi_cs_pin_); + GPIO_InitTypeDef GPIO_InitStruct; + GPIO_InitStruct.Pin = abs_spi_cs_pin_; + GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + HAL_GPIO_Init(abs_spi_cs_port_, &GPIO_InitStruct); + + // Write pin high + HAL_GPIO_WritePin(abs_spi_cs_port_, abs_spi_cs_pin_, GPIO_PIN_SET); +} + bool Encoder::update() { // update internal encoder state. int32_t delta_enc = 0; - switch (config_.mode) { + int32_t pos_abs_latched = pos_abs_; //LATCH + + switch (mode_) { case MODE_INCREMENTAL: { //TODO: use count_in_cpr_ instead as shadow_count_ can overflow //or use 64 bit @@ -331,6 +476,28 @@ bool Encoder::update() { delta_enc -= 6283; } break; + case MODE_SPI_ABS_RLS: + case MODE_SPI_ABS_AMS: + case MODE_SPI_ABS_CUI: + case MODE_SPI_ABS_AEAT: { + if (abs_spi_pos_updated_ == false) { + // Low pass filter the error + spi_error_rate_ += current_meas_period * (1.0f - spi_error_rate_); + if (spi_error_rate_ > 0.005f) + set_error(ERROR_ABS_SPI_COM_FAIL); + } else { + // Low pass filter the error + spi_error_rate_ += current_meas_period * (0.0f - spi_error_rate_); + } + + abs_spi_pos_updated_ = false; + delta_enc = pos_abs_latched - count_in_cpr_; //LATCH + delta_enc = mod(delta_enc, config_.cpr); + if (delta_enc > config_.cpr/2) { + delta_enc -= config_.cpr; + } + + }break; default: { set_error(ERROR_UNSUPPORTED_ENCODER_MODE); return false; @@ -341,38 +508,51 @@ bool Encoder::update() { count_in_cpr_ += delta_enc; count_in_cpr_ = mod(count_in_cpr_, config_.cpr); + if(mode_ & MODE_FLAG_ABS) + count_in_cpr_ = pos_abs_latched; + //// run pll (for now pll is in units of encoder counts) // Predict current pos - pos_estimate_ += current_meas_period * vel_estimate_; - pos_cpr_ += current_meas_period * vel_estimate_; + pos_estimate_counts_ += current_meas_period * vel_estimate_counts_; + pos_cpr_counts_ += current_meas_period * vel_estimate_counts_; // discrete phase detector - float delta_pos = (float)(shadow_count_ - (int32_t)floorf(pos_estimate_)); - float delta_pos_cpr = (float)(count_in_cpr_ - (int32_t)floorf(pos_cpr_)); - delta_pos_cpr = wrap_pm(delta_pos_cpr, 0.5f * (float)(config_.cpr)); + float delta_pos_counts = (float)(shadow_count_ - (int32_t)std::floor(pos_estimate_counts_)); + float delta_pos_cpr_counts = (float)(count_in_cpr_ - (int32_t)std::floor(pos_cpr_counts_)); + delta_pos_cpr_counts = wrap_pm(delta_pos_cpr_counts, 0.5f * (float)(config_.cpr)); // pll feedback - pos_estimate_ += current_meas_period * pll_kp_ * delta_pos; - pos_cpr_ += current_meas_period * pll_kp_ * delta_pos_cpr; - pos_cpr_ = fmodf_pos(pos_cpr_, (float)(config_.cpr)); - vel_estimate_ += current_meas_period * pll_ki_ * delta_pos_cpr; + pos_estimate_counts_ += current_meas_period * pll_kp_ * delta_pos_counts; + pos_cpr_counts_ += current_meas_period * pll_kp_ * delta_pos_cpr_counts; + pos_cpr_counts_ = fmodf_pos(pos_cpr_counts_, (float)(config_.cpr)); + vel_estimate_counts_ += current_meas_period * pll_ki_ * delta_pos_cpr_counts; bool snap_to_zero_vel = false; - if (fabsf(vel_estimate_) < 0.5f * current_meas_period * pll_ki_) { - vel_estimate_ = 0.0f; //align delta-sigma on zero to prevent jitter + if (std::abs(vel_estimate_counts_) < 0.5f * current_meas_period * pll_ki_) { + vel_estimate_counts_ = 0.0f; //align delta-sigma on zero to prevent jitter snap_to_zero_vel = true; } + // Outputs from Encoder for Controller + float pos_cpr_last = pos_cpr_; + pos_estimate_ = pos_estimate_counts_ / (float)config_.cpr; + vel_estimate_ = vel_estimate_counts_ / (float)config_.cpr; + pos_cpr_= pos_cpr_counts_ / (float)config_.cpr; + float delta_pos_cpr = wrap_pm(pos_cpr_ - pos_cpr_last, 0.5f); + pos_circular_ += delta_pos_cpr; + pos_circular_ = fmodf_pos(pos_circular_, axis_->controller_.config_.circular_setpoint_range); + //// run encoder count interpolation int32_t corrected_enc = count_in_cpr_ - config_.offset; // if we are stopped, make sure we don't randomly drift if (snap_to_zero_vel || !config_.enable_phase_interpolation) { interpolation_ = 0.5f; // reset interpolation if encoder edge comes + // TODO: This isn't correct. At high velocities the first phase in this count may very well not be at the edge. } else if (delta_enc > 0) { interpolation_ = 0.0f; } else if (delta_enc < 0) { interpolation_ = 1.0f; } else { // Interpolate (predict) between encoder counts using vel_estimate, - interpolation_ += current_meas_period * vel_estimate_; + interpolation_ += current_meas_period * vel_estimate_counts_; // don't allow interpolation indicated position outside of [enc, enc+1) if (interpolation_ > 1.0f) interpolation_ = 1.0f; if (interpolation_ < 0.0f) interpolation_ = 0.0f; @@ -386,5 +566,7 @@ bool Encoder::update() { // ph = fmodf(ph, 2*M_PI); phase_ = wrap_pm_pi(ph); + vel_estimate_valid_ = true; + pos_estimate_valid_ = true; return true; } diff --git a/Firmware/MotorControl/encoder.hpp b/Firmware/MotorControl/encoder.hpp index c2d32841..15be6576 100644 --- a/Firmware/MotorControl/encoder.hpp +++ b/Firmware/MotorControl/encoder.hpp @@ -5,26 +5,12 @@ #error "This file should not be included directly. Include odrive_main.h instead." #endif -class Encoder { +class Encoder : public ODriveIntf::EncoderIntf { public: - enum Error_t { - ERROR_NONE = 0, - ERROR_UNSTABLE_GAIN = 0x01, - ERROR_CPR_OUT_OF_RANGE = 0x02, - ERROR_NO_RESPONSE = 0x04, - ERROR_UNSUPPORTED_ENCODER_MODE = 0x08, - ERROR_ILLEGAL_HALL_STATE = 0x10, - ERROR_INDEX_NOT_FOUND_YET = 0x20, - }; - - enum Mode_t { - MODE_INCREMENTAL, - MODE_HALL, - MODE_SINCOS - }; + const uint32_t MODE_FLAG_ABS = 0x100; struct Config_t { - Encoder::Mode_t mode = Encoder::MODE_INCREMENTAL; + Mode mode = MODE_INCREMENTAL; bool use_index = false; bool pre_calibrated = false; // If true, this means the offset stored in // configuration is valid and does not need @@ -43,13 +29,24 @@ public: bool find_idx_on_lockin_only = false; // Only be sensitive during lockin scan constant vel state bool idx_search_unidirectional = false; // Only allow index search in known direction bool ignore_illegal_hall_state = false; // dont error on bad states like 000 or 111 + uint16_t abs_spi_cs_gpio_pin = 1; + uint16_t sincos_gpio_pin_sin = 3; + uint16_t sincos_gpio_pin_cos = 4; + + // custom setters + Encoder* parent = nullptr; + void set_use_index(bool value) { use_index = value; parent->set_idx_subscribe(); } + void set_find_idx_on_lockin_only(bool value) { find_idx_on_lockin_only = value; parent->set_idx_subscribe(); } + void set_abs_spi_cs_gpio_pin(uint16_t value) { abs_spi_cs_gpio_pin = value; parent->abs_spi_cs_pin_init(); } + void set_pre_calibrated(bool value) { pre_calibrated = value; parent->check_pre_calibrated(); } + void set_bandwidth(float value) { bandwidth = value; parent->update_pll_gains(); } }; Encoder(const EncoderHardwareConfig_t& hw_config, - Config_t& config); + Config_t& config, const Motor::Config_t& motor_config); void setup(); - void set_error(Error_t error); + void set_error(Error error); bool do_checks(); void enc_index_cb(); @@ -67,25 +64,33 @@ public: void sample_now(); bool update(); - - const EncoderHardwareConfig_t& hw_config_; Config_t& config_; Axis* axis_ = nullptr; // set by Axis constructor - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; bool index_found_ = false; bool is_ready_ = false; int32_t shadow_count_ = 0; int32_t count_in_cpr_ = 0; float interpolation_ = 0.0f; - float phase_ = 0.0f; // [count] - float pos_estimate_ = 0.0f; // [count] - float pos_cpr_ = 0.0f; // [count] - float vel_estimate_ = 0.0f; // [count/s] + float phase_ = 0.0f; // [count] + float pos_estimate_counts_ = 0.0f; // [count] + float pos_cpr_counts_ = 0.0f; // [count] + float vel_estimate_counts_ = 0.0f; // [count/s] float pll_kp_ = 0.0f; // [count/s / count] float pll_ki_ = 0.0f; // [(count/s^2) / count] float calib_scan_response_ = 0.0f; // debug report from offset calib + int32_t pos_abs_ = 0; + float spi_error_rate_ = 0.0f; + + float pos_estimate_ = 0.0f; // [turn] + float vel_estimate_ = 0.0f; // [turn/s] + float pos_cpr_ = 0.0f; // [turn] + float pos_circular_ = 0.0f; // [turn] + + bool pos_estimate_valid_ = false; + bool vel_estimate_valid_ = false; int16_t tim_cnt_sample_ = 0; // // Updated by low_level pwm_adc_cb @@ -93,49 +98,22 @@ public: float sincos_sample_s_ = 0.0f; float sincos_sample_c_ = 0.0f; - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_property("is_ready", &is_ready_), - make_protocol_property("index_found", const_cast(&index_found_)), - make_protocol_property("shadow_count", &shadow_count_), - make_protocol_property("count_in_cpr", &count_in_cpr_), - make_protocol_property("interpolation", &interpolation_), - make_protocol_ro_property("phase", &phase_), - make_protocol_property("pos_estimate", &pos_estimate_), - make_protocol_property("pos_cpr", &pos_cpr_), - make_protocol_ro_property("hall_state", &hall_state_), - make_protocol_property("vel_estimate", &vel_estimate_), - make_protocol_ro_property("calib_scan_response", &calib_scan_response_), - // make_protocol_property("pll_kp", &pll_kp_), - // make_protocol_property("pll_ki", &pll_ki_), - make_protocol_object("config", - make_protocol_property("mode", &config_.mode), - make_protocol_property("use_index", &config_.use_index, - [](void* ctx) { static_cast(ctx)->set_idx_subscribe(); }, this), - make_protocol_property("find_idx_on_lockin_only", &config_.find_idx_on_lockin_only, - [](void* ctx) { static_cast(ctx)->set_idx_subscribe(); }, this), - make_protocol_property("pre_calibrated", &config_.pre_calibrated, - [](void* ctx) { static_cast(ctx)->check_pre_calibrated(); }, this), - make_protocol_property("zero_count_on_find_idx", &config_.zero_count_on_find_idx), - make_protocol_property("cpr", &config_.cpr), - make_protocol_property("offset", &config_.offset), - make_protocol_property("offset_float", &config_.offset_float), - make_protocol_property("enable_phase_interpolation", &config_.enable_phase_interpolation), - make_protocol_property("bandwidth", &config_.bandwidth, - [](void* ctx) { static_cast(ctx)->update_pll_gains(); }, this), - make_protocol_property("calib_range", &config_.calib_range), - make_protocol_property("calib_scan_distance", &config_.calib_scan_distance), - make_protocol_property("calib_scan_omega", &config_.calib_scan_omega), - make_protocol_property("idx_search_unidirectional", &config_.idx_search_unidirectional), - make_protocol_property("ignore_illegal_hall_state", &config_.ignore_illegal_hall_state) - ), - make_protocol_function("set_linear_count", *this, &Encoder::set_linear_count, "count") - ); + bool abs_spi_init(); + bool abs_spi_start_transaction(); + void abs_spi_cb(); + void abs_spi_cs_pin_init(); + uint16_t abs_spi_dma_tx_[1] = {0xFFFF}; + uint16_t abs_spi_dma_rx_[1]; + bool abs_spi_pos_updated_ = false; + Mode mode_ = MODE_INCREMENTAL; + GPIO_TypeDef* abs_spi_cs_port_; + uint16_t abs_spi_cs_pin_; + uint32_t abs_spi_cr1; + uint32_t abs_spi_cr2; + + constexpr float getCoggingRatio(){ + return 1.0f / 3600.0f; } }; -DEFINE_ENUM_FLAG_OPERATORS(Encoder::Error_t) - #endif // __ENCODER_HPP diff --git a/Firmware/MotorControl/endstop.cpp b/Firmware/MotorControl/endstop.cpp new file mode 100644 index 00000000..7febba16 --- /dev/null +++ b/Firmware/MotorControl/endstop.cpp @@ -0,0 +1,55 @@ +#include + +Endstop::Endstop(Endstop::Config_t& config) + : config_(config) { + update_config(); + debounceTimer_.setIncrement(current_meas_period); +} + + +void Endstop::update() { + debounceTimer_.update(); + if (config_.enabled) { + bool last_pin_state = pin_state_; + + uint16_t gpio_pin = get_gpio_pin_by_pin(config_.gpio_num); + GPIO_TypeDef* gpio_port = get_gpio_port_by_pin(config_.gpio_num); + pin_state_ = HAL_GPIO_ReadPin(gpio_port, gpio_pin); + + // If the pin state has changed, reset the timer + if (pin_state_ != last_pin_state) + debounceTimer_.reset(); + + if (debounceTimer_.expired()) + endstop_state_ = config_.is_active_high ? pin_state_ : !pin_state_; // endstop_state is the logical state + } else { + endstop_state_ = false; + } +} + +bool Endstop::get_state() { + return endstop_state_; +} + +void Endstop::update_config() { + set_enabled(config_.enabled); + debounceTimer_.setIncrement(config_.debounce_ms * 0.001f); +} + +void Endstop::set_enabled(bool enable) { + debounceTimer_.reset(); + if (config_.gpio_num != 0) { + uint16_t gpio_pin = get_gpio_pin_by_pin(config_.gpio_num); + GPIO_TypeDef* gpio_port = get_gpio_port_by_pin(config_.gpio_num); + if (enable) { + HAL_GPIO_DeInit(gpio_port, gpio_pin); + GPIO_InitTypeDef GPIO_InitStruct; + GPIO_InitStruct.Pin = gpio_pin; + GPIO_InitStruct.Mode = GPIO_MODE_INPUT; + GPIO_InitStruct.Pull = config_.pullup ? GPIO_PULLUP : GPIO_PULLDOWN; + HAL_GPIO_Init(gpio_port, &GPIO_InitStruct); + debounceTimer_.start(); + } else + debounceTimer_.stop(); + } +} \ No newline at end of file diff --git a/Firmware/MotorControl/endstop.hpp b/Firmware/MotorControl/endstop.hpp new file mode 100644 index 00000000..f108dffe --- /dev/null +++ b/Firmware/MotorControl/endstop.hpp @@ -0,0 +1,40 @@ +#ifndef __ENDSTOP_HPP +#define __ENDSTOP_HPP + +#include "timer.hpp" +class Endstop { + public: + struct Config_t { + float offset = 0; + uint32_t debounce_ms = 50; + uint16_t gpio_num = 0; + bool enabled = false; + bool is_active_high = false; + bool pullup = true; + + // custom setters + Endstop* parent = nullptr; + void set_gpio_num(uint16_t value) { gpio_num = value; parent->update_config(); } + void set_enabled(uint32_t value) { enabled = value; parent->update_config(); } + void set_debounce_ms(uint32_t value) { debounce_ms = value; parent->update_config(); } + }; + + explicit Endstop(Endstop::Config_t& config); + + Endstop::Config_t& config_; + Axis* axis_ = nullptr; + + void update_config(); + void set_enabled(bool enabled); + + void update(); + bool get_state(); + + bool endstop_state_ = false; + + private: + bool pin_state_ = false; + float pos_when_pressed_ = 0.0f; + Timer debounceTimer_; +}; +#endif \ No newline at end of file diff --git a/Firmware/MotorControl/gpio_utils.hpp b/Firmware/MotorControl/gpio_utils.hpp new file mode 100644 index 00000000..fb70f46e --- /dev/null +++ b/Firmware/MotorControl/gpio_utils.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "gpio.h" +constexpr GPIO_TypeDef* get_gpio_port_by_pin(uint16_t GPIO_pin){ + switch(GPIO_pin){ + case 1: return GPIO_1_GPIO_Port; break; + case 2: return GPIO_2_GPIO_Port; break; + case 3: return GPIO_3_GPIO_Port; break; + case 4: return GPIO_4_GPIO_Port; break; +#ifdef GPIO_5_GPIO_Port + case 5: return GPIO_5_GPIO_Port; break; +#endif +#ifdef GPIO_6_GPIO_Port + case 6: return GPIO_6_GPIO_Port; break; +#endif +#ifdef GPIO_7_GPIO_Port + case 7: return GPIO_7_GPIO_Port; break; +#endif +#ifdef GPIO_8_GPIO_Port + case 8: return GPIO_8_GPIO_Port; break; +#endif + default: return GPIO_1_GPIO_Port; + } +} + +constexpr uint16_t get_gpio_pin_by_pin(uint16_t GPIO_pin){ + switch(GPIO_pin){ + case 1: return GPIO_1_Pin; break; + case 2: return GPIO_2_Pin; break; + case 3: return GPIO_3_Pin; break; + case 4: return GPIO_4_Pin; break; +#ifdef GPIO_5_Pin + case 5: return GPIO_5_Pin; break; +#endif +#ifdef GPIO_6_Pin + case 6: return GPIO_6_Pin; break; +#endif +#ifdef GPIO_7_Pin + case 7: return GPIO_7_Pin; break; +#endif +#ifdef GPIO_8_Pin + case 8: return GPIO_8_Pin; break; +#endif + default: return GPIO_1_Pin; + } +} diff --git a/Firmware/MotorControl/low_level.cpp b/Firmware/MotorControl/low_level.cpp index 6125c99c..d447539d 100644 --- a/Firmware/MotorControl/low_level.cpp +++ b/Firmware/MotorControl/low_level.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include #include "odrive_main.h" @@ -28,14 +28,16 @@ /* Private macros ------------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/ /* Global constant data ------------------------------------------------------*/ -const float adc_full_scale = (float)(1 << 12); -const float adc_ref_voltage = 3.3f; +constexpr float adc_full_scale = static_cast(1UL << 12UL); +constexpr float adc_ref_voltage = 3.3f; /* Global variables ----------------------------------------------------------*/ // This value is updated by the DC-bus reading ADC. // Arbitrary non-zero inital value to avoid division by zero if ADC reading is late float vbus_voltage = 12.0f; +float ibus_ = 0.0f; // exposed for monitoring only bool brake_resistor_armed = false; +bool brake_resistor_saturated = false; /* Private constant data -----------------------------------------------------*/ static const GPIO_TypeDef* GPIOs_to_samp[] = { GPIOA, GPIOB, GPIOC }; static const int num_GPIO = sizeof(GPIOs_to_samp) / sizeof(GPIOs_to_samp[0]); @@ -82,7 +84,7 @@ static uint16_t GPIO_port_samples [2][num_GPIO]; */ // @brief Floats ALL phases immediately and disarms both motors and the brake resistor. -void low_level_fault(Motor::Error_t error) { +void low_level_fault(Motor::Error error) { // Disable all motors NOW! for (size_t i = 0; i < AXIS_COUNT; ++i) { safety_critical_disarm_motor_pwm(axes[i]->motor_); @@ -212,6 +214,7 @@ void start_adc_pwm() { // Ensure that debug halting of the core doesn't leave the motor PWM running __HAL_DBGMCU_FREEZE_TIM1(); __HAL_DBGMCU_FREEZE_TIM8(); + __HAL_DBGMCU_FREEZE_TIM13(); start_pwm(&htim1); start_pwm(&htim8); @@ -259,6 +262,20 @@ void start_pwm(TIM_HandleTypeDef* htim) { HAL_TIM_PWM_Start_IT(htim, TIM_CHANNEL_4); } +/* + * Initial intention of this function: + * Synchronize TIM1, TIM8 and TIM13 such that: + * 1. The triangle waveform of TIM1 leads the triangle waveform of TIM8 by a + * 90° phase shift. + * 2. The timer update events of TIM1 and TIM8 are symmetrically interleaved. + * 3. Each TIM13 reload coincides with a TIM1 lower update event. + * + * However right now this function only ensures point (1) and (3) but because + * TIM1 and TIM3 only trigger an update on every third reload, this does not + * imply (or even allow for) (2). + * + * TODO: revisit the timing topic in general. + */ void sync_timers(TIM_HandleTypeDef* htim_a, TIM_HandleTypeDef* htim_b, uint16_t TIM_CLOCKSOURCE_ITRx, uint16_t count_offset, TIM_HandleTypeDef* htim_refbase) { @@ -373,8 +390,16 @@ void start_general_purpose_adc() { // 21000kHz / (15+26) / 16 = 32kHz // The true frequency is slightly lower because of the injected vbus // measurements -float get_adc_voltage(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin) { - uint32_t channel = UINT32_MAX; +float get_adc_voltage(const GPIO_TypeDef* const GPIO_port, uint16_t GPIO_pin) { + const uint16_t channel = channel_from_gpio(GPIO_port, GPIO_pin); + return get_adc_voltage_channel(channel); +} + +// @brief Given a GPIO_port and pin return the associated adc_channel. +// returns UINT16_MAX if there is no adc_channel; +uint16_t channel_from_gpio(const GPIO_TypeDef* const GPIO_port, uint16_t GPIO_pin) +{ + uint16_t channel = UINT16_MAX; if (GPIO_port == GPIOA) { if (GPIO_pin == GPIO_PIN_0) channel = 0; @@ -411,6 +436,13 @@ float get_adc_voltage(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin) { else if (GPIO_pin == GPIO_PIN_5) channel = 15; } + return channel; +} + +// @brief Given an adc channel return the measured voltage. +// returns NaN if the channel is not valid. +float get_adc_voltage_channel(uint16_t channel) +{ if (channel < ADC_CHANNEL_COUNT) return ((float)adc_measurements_[channel]) * (adc_ref_voltage / adc_full_scale); else @@ -422,15 +454,10 @@ float get_adc_voltage(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin) { //-------------------------------- void vbus_sense_adc_cb(ADC_HandleTypeDef* hadc, bool injected) { - static const float voltage_scale = adc_ref_voltage * VBUS_S_DIVIDER_RATIO / adc_full_scale; + constexpr float voltage_scale = adc_ref_voltage * VBUS_S_DIVIDER_RATIO / adc_full_scale; // Only one conversion in sequence, so only rank1 uint32_t ADCValue = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1); vbus_voltage = ADCValue * voltage_scale; - if (axes[0] && !axes[0]->error_ && axes[1] && !axes[1]->error_) { - if (oscilloscope_pos >= OSCILLOSCOPE_SIZE) - oscilloscope_pos = 0; - oscilloscope[oscilloscope_pos++] = vbus_voltage; - } } static void decode_hall_samples(Encoder& enc, uint16_t GPIO_samples[num_GPIO]) { @@ -466,7 +493,7 @@ static void decode_hall_samples(Encoder& enc, uint16_t GPIO_samples[num_GPIO]) { // TODO: Document how the phasing is done, link to timing diagram void pwm_trig_adc_cb(ADC_HandleTypeDef* hadc, bool injected) { #define calib_tau 0.2f //@TOTO make more easily configurable - static const float calib_filter_k = CURRENT_MEAS_PERIOD / calib_tau; + constexpr float calib_filter_k = CURRENT_MEAS_PERIOD / calib_tau; // Ensure ADCs are expected ones to simplify the logic below if (!(hadc == &hadc2 || hadc == &hadc3)) { @@ -486,9 +513,9 @@ void pwm_trig_adc_cb(ADC_HandleTypeDef* hadc, bool injected) { // Check the timing of the sequencing if (current_meas_not_DC_CAL) - axis.motor_.log_timing(Motor::TIMING_LOG_ADC_CB_I); + axis.motor_.log_timing(TIMING_LOG_ADC_CB_I); else - axis.motor_.log_timing(Motor::TIMING_LOG_ADC_CB_DC); + axis.motor_.log_timing(TIMING_LOG_ADC_CB_DC); bool update_timings = false; if (hadc == &hadc2) { @@ -496,6 +523,15 @@ void pwm_trig_adc_cb(ADC_HandleTypeDef* hadc, bool injected) { update_timings = true; // update timings of M0 else if (&axis == axes[0] && !counting_down) update_timings = true; // update timings of M1 + + // TODO: this is out of place here. However when moving it somewhere + // else we have to consider the timing requirements to prevent the SPI + // transfers of axis0 and axis1 from conflicting. + // Also see comment on sync_timers. + if((current_meas_not_DC_CAL && !axis_num) || + (axis_num && !current_meas_not_DC_CAL)){ + axis.encoder_.abs_spi_start_transaction(); + } } // Load next timings for the motor that we're not currently sampling @@ -590,22 +626,46 @@ void update_brake_current() { Ibus_sum += axes[i]->motor_.current_control_.Ibus; } } - float brake_current = -Ibus_sum; - // Clip negative values to 0.0f - if (brake_current < 0.0f) brake_current = 0.0f; - float brake_duty = brake_current * board_config.brake_resistance / vbus_voltage; - - // Duty limit at 90% to allow bootstrap caps to charge - // If brake_duty is NaN, this expression will also evaluate to false - if ((brake_duty >= 0.0f) && (brake_duty <= 0.9f)) { - int high_on = static_cast(TIM_APB1_PERIOD_CLOCKS * (1.0f - brake_duty)); - int low_off = high_on - TIM_APB1_DEADTIME_CLOCKS; - if (low_off < 0) low_off = 0; - safety_critical_apply_brake_resistor_timings(low_off, high_on); - } else { - //shuts off all motors AND brake resistor, sets error code on all motors. - low_level_fault(Motor::ERROR_BRAKE_CURRENT_OUT_OF_RANGE); + + // Don't start braking until -Ibus > regen_current_allowed + float brake_current = -Ibus_sum - odrv.config_.max_regen_current; + float brake_duty = brake_current * odrv.config_.brake_resistance / vbus_voltage; + + if (odrv.config_.enable_dc_bus_overvoltage_ramp && (odrv.config_.brake_resistance > 0.0f) && (odrv.config_.dc_bus_overvoltage_ramp_start < odrv.config_.dc_bus_overvoltage_ramp_end)) { + brake_duty += std::fmax((vbus_voltage - odrv.config_.dc_bus_overvoltage_ramp_start) / (odrv.config_.dc_bus_overvoltage_ramp_end - odrv.config_.dc_bus_overvoltage_ramp_start), 0.0f); } + + if (std::isnan(brake_duty)) { + // Shuts off all motors AND brake resistor, sets error code on all motors. + low_level_fault(Motor::ERROR_BRAKE_DUTY_CYCLE_NAN); + return; + } + + if (brake_duty >= 0.95f) { + brake_resistor_saturated = true; + } + + // Duty limit at 95% to allow bootstrap caps to charge + brake_duty = std::clamp(brake_duty, 0.0f, 0.95f); + + // Special handling to avoid the case 0.0/0.0 == NaN. + Ibus_sum += brake_duty ? (brake_duty * vbus_voltage / odrv.config_.brake_resistance) : 0.0f; + + ibus_ += odrv.ibus_report_filter_k_ * (Ibus_sum - ibus_); + + if (Ibus_sum > odrv.config_.dc_max_positive_current) { + low_level_fault(Motor::ERROR_DC_BUS_OVER_CURRENT); + return; + } + if (Ibus_sum < odrv.config_.dc_max_negative_current) { + low_level_fault(Motor::ERROR_DC_BUS_OVER_REGEN_CURRENT); + return; + } + + int high_on = (int)(TIM_APB1_PERIOD_CLOCKS * (1.0f - brake_duty)); + int low_off = high_on - TIM_APB1_DEADTIME_CLOCKS; + if (low_off < 0) low_off = 0; + safety_critical_apply_brake_resistor_timings(low_off, high_on); } @@ -670,7 +730,7 @@ void pwm_in_init() { #else int gpio_num = 4; { #endif - if (is_endpoint_ref_valid(board_config.pwm_mappings[gpio_num - 1].endpoint)) { + if (fibre::is_endpoint_ref_valid(odrv.config_.pwm_mappings[gpio_num - 1].endpoint)) { GPIO_InitStruct.Pin = get_gpio_pin_by_pin(gpio_num); HAL_GPIO_DeInit(get_gpio_port_by_pin(gpio_num), get_gpio_pin_by_pin(gpio_num)); HAL_GPIO_Init(get_gpio_port_by_pin(gpio_num), &GPIO_InitStruct); @@ -697,14 +757,10 @@ void handle_pulse(int gpio_num, uint32_t high_time) { if (high_time > PWM_MAX_HIGH_TIME) high_time = PWM_MAX_HIGH_TIME; float fraction = (float)(high_time - PWM_MIN_HIGH_TIME) / (float)(PWM_MAX_HIGH_TIME - PWM_MIN_HIGH_TIME); - float value = board_config.pwm_mappings[gpio_num - 1].min + - (fraction * (board_config.pwm_mappings[gpio_num - 1].max - board_config.pwm_mappings[gpio_num - 1].min)); + float value = odrv.config_.pwm_mappings[gpio_num - 1].min + + (fraction * (odrv.config_.pwm_mappings[gpio_num - 1].max - odrv.config_.pwm_mappings[gpio_num - 1].min)); - Endpoint* endpoint = get_endpoint(board_config.pwm_mappings[gpio_num - 1].endpoint); - if (!endpoint) - return; - - endpoint->set_from_float(value); + fibre::set_endpoint_from_float(odrv.config_.pwm_mappings[gpio_num - 1].endpoint, value); } void pwm_in_cb(int channel, uint32_t timestamp) { @@ -735,24 +791,32 @@ static void update_analog_endpoint(const struct PWMMapping_t *map, int gpio) { float fraction = get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)) / 3.3f; float value = map->min + (fraction * (map->max - map->min)); - get_endpoint(map->endpoint)->set_from_float(value); + fibre::set_endpoint_from_float(map->endpoint, value); } static void analog_polling_thread(void *) { while (true) { for (int i = 0; i < GPIO_COUNT; i++) { - struct PWMMapping_t *map = &board_config.analog_mappings[i]; + struct PWMMapping_t *map = &odrv.config_.analog_mappings[i]; - if (is_endpoint_ref_valid(map->endpoint)) + if (fibre::is_endpoint_ref_valid(map->endpoint)) update_analog_endpoint(map, i + 1); } osDelay(10); } } -void start_analog_thread() -{ - osThreadDef(thread_def, analog_polling_thread, osPriorityLow, 0, 4*512); +void start_analog_thread() { + osThreadDef(thread_def, analog_polling_thread, osPriorityLow, 0, 512 / sizeof(StackType_t)); osThreadCreate(osThread(thread_def), NULL); } + + +void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) +{ + if(hspi->pRxBuffPtr == (uint8_t*)axes[0]->encoder_.abs_spi_dma_rx_) + axes[0]->encoder_.abs_spi_cb(); + else if (hspi->pRxBuffPtr == (uint8_t*)axes[1]->encoder_.abs_spi_dma_rx_) + axes[1]->encoder_.abs_spi_cb(); +} diff --git a/Firmware/MotorControl/low_level.h b/Firmware/MotorControl/low_level.h index 503b98e1..11494cbf 100644 --- a/Firmware/MotorControl/low_level.h +++ b/Firmware/MotorControl/low_level.h @@ -22,7 +22,9 @@ extern const float adc_full_scale; extern const float adc_ref_voltage; /* Exported variables --------------------------------------------------------*/ extern float vbus_voltage; +extern float ibus_; extern bool brake_resistor_armed; +extern bool brake_resistor_saturated; extern uint16_t adc_measurements_[ADC_CHANNEL_COUNT]; /* Exported macro ------------------------------------------------------------*/ /* Exported functions --------------------------------------------------------*/ @@ -49,7 +51,9 @@ void sync_timers(TIM_HandleTypeDef* htim_a, TIM_HandleTypeDef* htim_b, uint16_t TIM_CLOCKSOURCE_ITRx, uint16_t count_offset, TIM_HandleTypeDef* htim_refbase = nullptr); void start_general_purpose_adc(); -float get_adc_voltage(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin); +float get_adc_voltage(const GPIO_TypeDef* const GPIO_port, uint16_t GPIO_pin); +uint16_t channel_from_gpio(const GPIO_TypeDef* const GPIO_port, uint16_t GPIO_pin); +float get_adc_voltage_channel(uint16_t channel); void pwm_in_init(); void start_analog_thread(); diff --git a/Firmware/MotorControl/main.cpp b/Firmware/MotorControl/main.cpp index d5acf252..2809f5da 100644 --- a/Firmware/MotorControl/main.cpp +++ b/Firmware/MotorControl/main.cpp @@ -3,45 +3,60 @@ #include "odrive_main.h" #include "nvm_config.hpp" +#include "usart.h" #include "freertos_vars.h" #include #include #include +#include -BoardConfig_t board_config; +ODriveCAN::Config_t can_config; Encoder::Config_t encoder_configs[AXIS_COUNT]; SensorlessEstimator::Config_t sensorless_configs[AXIS_COUNT]; Controller::Config_t controller_configs[AXIS_COUNT]; Motor::Config_t motor_configs[AXIS_COUNT]; +OnboardThermistorCurrentLimiter::Config_t fet_thermistor_configs[AXIS_COUNT]; +OffboardThermistorCurrentLimiter::Config_t motor_thermistor_configs[AXIS_COUNT]; Axis::Config_t axis_configs[AXIS_COUNT]; TrapezoidalTrajectory::Config_t trap_configs[AXIS_COUNT]; -bool user_config_loaded_; +Endstop::Config_t min_endstop_configs[AXIS_COUNT]; +Endstop::Config_t max_endstop_configs[AXIS_COUNT]; -SystemStats_t system_stats_ = { 0 }; - -Axis *axes[AXIS_COUNT]; +std::array axes; +ODriveCAN *odCAN = nullptr; +ODrive odrv{}; typedef Config< BoardConfig_t, + ODriveCAN::Config_t, Encoder::Config_t[AXIS_COUNT], SensorlessEstimator::Config_t[AXIS_COUNT], Controller::Config_t[AXIS_COUNT], Motor::Config_t[AXIS_COUNT], + OnboardThermistorCurrentLimiter::Config_t[AXIS_COUNT], + OffboardThermistorCurrentLimiter::Config_t[AXIS_COUNT], TrapezoidalTrajectory::Config_t[AXIS_COUNT], + Endstop::Config_t[AXIS_COUNT], + Endstop::Config_t[AXIS_COUNT], Axis::Config_t[AXIS_COUNT]> ConfigFormat; -void save_configuration(void) { +void ODrive::save_configuration(void) { if (ConfigFormat::safe_store_config( - &board_config, + &odrv.config_, + &can_config, &encoder_configs, &sensorless_configs, &controller_configs, &motor_configs, + &fet_thermistor_configs, + &motor_thermistor_configs, &trap_configs, + &min_endstop_configs, + &max_endstop_configs, &axis_configs)) { - //printf("saving configuration failed\r\n"); osDelay(5); + printf("saving configuration failed\r\n"); osDelay(5); } else { - user_config_loaded_ = true; + odrv.user_config_loaded_ = true; } } @@ -49,37 +64,56 @@ extern "C" int load_configuration(void) { // Try to load configs if (NVM_init() || ConfigFormat::safe_load_config( - &board_config, + &odrv.config_, + &can_config, &encoder_configs, &sensorless_configs, &controller_configs, &motor_configs, + &fet_thermistor_configs, + &motor_thermistor_configs, &trap_configs, + &min_endstop_configs, + &max_endstop_configs, &axis_configs)) { //If loading failed, restore defaults - board_config = BoardConfig_t(); + odrv.config_ = BoardConfig_t(); + can_config = ODriveCAN::Config_t(); for (size_t i = 0; i < AXIS_COUNT; ++i) { encoder_configs[i] = Encoder::Config_t(); sensorless_configs[i] = SensorlessEstimator::Config_t(); controller_configs[i] = Controller::Config_t(); motor_configs[i] = Motor::Config_t(); + fet_thermistor_configs[i] = OnboardThermistorCurrentLimiter::Config_t(); + motor_thermistor_configs[i] = OffboardThermistorCurrentLimiter::Config_t(); trap_configs[i] = TrapezoidalTrajectory::Config_t(); axis_configs[i] = Axis::Config_t(); // Default step/dir pins are different, so we need to explicitly load them Axis::load_default_step_dir_pin_config(hw_configs[i].axis_config, &axis_configs[i]); + Axis::load_default_can_id(i, axis_configs[i]); + min_endstop_configs[i] = Endstop::Config_t(); + max_endstop_configs[i] = Endstop::Config_t(); + controller_configs[i].load_encoder_axis = i; } } else { - user_config_loaded_ = true; + odrv.user_config_loaded_ = true; } - return user_config_loaded_; + return odrv.user_config_loaded_; } -void erase_configuration(void) { +void ODrive::erase_configuration(void) { NVM_erase(); + + // FIXME: this reboot is a workaround because we don't want the next save_configuration + // to write back the old configuration from RAM to NVM. The proper action would + // be to reset the values in RAM to default. However right now that's not + // practical because several startup actions depend on the config. The + // other problem is that the stack overflows if we reset to default here. + NVIC_SystemReset(); } -void enter_dfu_mode() { - if ((hw_version_major == 3) && (hw_version_minor >= 5)) { +void ODrive::enter_dfu_mode() { + if ((hw_version_major_ == 3) && (hw_version_minor_ >= 5)) { __asm volatile ("CPSID I\n\t":::"memory"); // disable interrupts _reboot_cookie = 0xDEADBEEF; NVIC_SystemReset(); @@ -96,30 +130,9 @@ void enter_dfu_mode() { } } -extern "C" { -int odrive_main(void); -void vApplicationStackOverflowHook(xTaskHandle *pxTask, signed portCHAR *pcTaskName) { - for (;;); // TODO: safe action -} -void vApplicationIdleHook(void) { - if (system_stats_.fully_booted) { - system_stats_.uptime = xTaskGetTickCount(); - system_stats_.min_heap_space = xPortGetMinimumEverFreeHeapSize(); - system_stats_.min_stack_space_comms = uxTaskGetStackHighWaterMark(comm_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_axis0 = uxTaskGetStackHighWaterMark(axes[0]->thread_id_) * sizeof(StackType_t); - system_stats_.min_stack_space_axis1 = uxTaskGetStackHighWaterMark(axes[1]->thread_id_) * sizeof(StackType_t); - system_stats_.min_stack_space_usb = uxTaskGetStackHighWaterMark(usb_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_uart = uxTaskGetStackHighWaterMark(uart_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_usb_irq = uxTaskGetStackHighWaterMark(usb_irq_thread) * sizeof(StackType_t); - system_stats_.min_stack_space_startup = uxTaskGetStackHighWaterMark(defaultTaskHandle) * sizeof(StackType_t); - } -} -} - -int odrive_main(void) { - +extern "C" int construct_objects(){ #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 - if (board_config.enable_i2c_instead_of_can) { + if (odrv.config_.enable_i2c_instead_of_can) { // Set up the direction GPIO as input GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; @@ -142,6 +155,10 @@ int odrive_main(void) { #endif MX_CAN1_Init(); + HAL_UART_DeInit(&huart4); + huart4.Init.BaudRate = odrv.config_.uart_baudrate; + HAL_UART_Init(&huart4); + // Init general user ADC on some GPIOs. GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; @@ -160,25 +177,80 @@ int odrive_main(void) { #endif // Construct all objects. + odCAN = new ODriveCAN(can_config, &hcan1); for (size_t i = 0; i < AXIS_COUNT; ++i) { Encoder *encoder = new Encoder(hw_configs[i].encoder_config, - encoder_configs[i]); + encoder_configs[i], motor_configs[i]); SensorlessEstimator *sensorless_estimator = new SensorlessEstimator(sensorless_configs[i]); Controller *controller = new Controller(controller_configs[i]); + + OnboardThermistorCurrentLimiter *fet_thermistor = new OnboardThermistorCurrentLimiter(hw_configs[i].thermistor_config, + fet_thermistor_configs[i]); + OffboardThermistorCurrentLimiter *motor_thermistor = new OffboardThermistorCurrentLimiter(motor_thermistor_configs[i]); + Motor *motor = new Motor(hw_configs[i].motor_config, hw_configs[i].gate_driver_config, motor_configs[i]); TrapezoidalTrajectory *trap = new TrapezoidalTrajectory(trap_configs[i]); + Endstop *min_endstop = new Endstop(min_endstop_configs[i]); + Endstop *max_endstop = new Endstop(max_endstop_configs[i]); axes[i] = new Axis(i, hw_configs[i].axis_config, axis_configs[i], - *encoder, *sensorless_estimator, *controller, *motor, *trap); + *encoder, *sensorless_estimator, *controller, *fet_thermistor, + *motor_thermistor, *motor, *trap, *min_endstop, *max_endstop); + + controller_configs[i].parent = controller; + encoder_configs[i].parent = encoder; + motor_thermistor_configs[i].parent = motor_thermistor; + motor_configs[i].parent = motor; + min_endstop_configs[i].parent = min_endstop; + max_endstop_configs[i].parent = max_endstop; + axis_configs[i].parent = axes[i]; } - + return 0; +} + +extern "C" { +int odrive_main(void); +void vApplicationStackOverflowHook(xTaskHandle *pxTask, signed portCHAR *pcTaskName) { + for(auto& axis : axes){ + safety_critical_disarm_motor_pwm(axis->motor_); + } + safety_critical_disarm_brake_resistor(); + for (;;); // TODO: safe action +} +void vApplicationIdleHook(void) { + if (odrv.system_stats_.fully_booted) { + odrv.system_stats_.uptime = xTaskGetTickCount(); + odrv.system_stats_.min_heap_space = xPortGetMinimumEverFreeHeapSize(); + odrv.system_stats_.min_stack_space_comms = uxTaskGetStackHighWaterMark(comm_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_axis0 = uxTaskGetStackHighWaterMark(axes[0]->thread_id_) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_axis1 = uxTaskGetStackHighWaterMark(axes[1]->thread_id_) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_usb = uxTaskGetStackHighWaterMark(usb_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_uart = uxTaskGetStackHighWaterMark(uart_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_usb_irq = uxTaskGetStackHighWaterMark(usb_irq_thread) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_startup = uxTaskGetStackHighWaterMark(defaultTaskHandle) * sizeof(StackType_t); + odrv.system_stats_.min_stack_space_can = uxTaskGetStackHighWaterMark(odCAN->thread_id_) * sizeof(StackType_t); + + // Actual usage, in bytes, so we don't have to math + odrv.system_stats_.stack_usage_axis0 = axes[0]->stack_size_ - odrv.system_stats_.min_stack_space_axis0; + odrv.system_stats_.stack_usage_axis1 = axes[1]->stack_size_ - odrv.system_stats_.min_stack_space_axis1; + odrv.system_stats_.stack_usage_comms = stack_size_comm_thread - odrv.system_stats_.min_stack_space_comms; + odrv.system_stats_.stack_usage_usb = stack_size_usb_thread - odrv.system_stats_.min_stack_space_usb; + odrv.system_stats_.stack_usage_uart = stack_size_uart_thread - odrv.system_stats_.min_stack_space_uart; + odrv.system_stats_.stack_usage_usb_irq = stack_size_usb_irq_thread - odrv.system_stats_.min_stack_space_usb_irq; + odrv.system_stats_.stack_usage_startup = stack_size_default_task - odrv.system_stats_.min_stack_space_startup; + odrv.system_stats_.stack_usage_can = odCAN->stack_size_ - odrv.system_stats_.min_stack_space_can; + } +} +} + +int odrive_main(void) { // Start ADC for temperature measurements and user measurements start_general_purpose_adc(); // TODO: make dynamically reconfigurable #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 - if (board_config.enable_uart) { + if (odrv.config_.enable_uart) { SetGPIO12toUART(); } #endif @@ -195,6 +267,10 @@ int odrive_main(void) { axes[i]->setup(); } + for(auto& axis : axes){ + axis->encoder_.setup(); + } + // Start PWM and enable adc interrupts/callbacks start_adc_pwm(); @@ -215,6 +291,6 @@ int odrive_main(void) { start_analog_thread(); - system_stats_.fully_booted = true; + odrv.system_stats_.fully_booted = true; return 0; } diff --git a/Firmware/MotorControl/motor.cpp b/Firmware/MotorControl/motor.cpp index 2a9cd703..c7e50128 100644 --- a/Firmware/MotorControl/motor.cpp +++ b/Firmware/MotorControl/motor.cpp @@ -50,6 +50,7 @@ bool Motor::arm() { void Motor::reset_current_control() { current_control_.v_current_control_integral_d = 0.0f; current_control_.v_current_control_integral_q = 0.0f; + current_control_.acim_rotor_flux = 0.0f; } // @brief Tune the current controller based on phase resistance and inductance @@ -72,9 +73,9 @@ void Motor::DRV8301_setup() { // Solve for exact gain, then snap down to have equal or larger range as requested // or largest possible range otherwise - static const float kMargin = 0.90f; - static const float kTripMargin = 1.0f; // Trip level is at edge of linear range of amplifer - static const float max_output_swing = 1.35f; // [V] out of amplifier + constexpr float kMargin = 0.90f; + constexpr float kTripMargin = 1.0f; // Trip level is at edge of linear range of amplifer + constexpr float max_output_swing = 1.35f; // [V] out of amplifier float max_unity_gain_current = kMargin * max_output_swing * hw_config_.shunt_conductance; // [A] float requested_gain = max_unity_gain_current / config_.requested_current_range; // [V/V] @@ -126,7 +127,7 @@ bool Motor::check_DRV_fault() { GPIO_PinState nFAULT_state = HAL_GPIO_ReadPin(gate_driver_config_.nFAULT_port, gate_driver_config_.nFAULT_pin); if (nFAULT_state == GPIO_PIN_RESET) { // Update DRV Fault Code - drv_fault_ = DRV8301_getFaultType(&gate_driver_); + gate_driver_exported_.drv_fault = (GateDriverIntf::DrvFault)DRV8301_getFaultType(&gate_driver_); // Update/Cache all SPI device registers // DRV_SPI_8301_Vars_t* local_regs = &gate_driver_regs_; // local_regs->RcvCmd = true; @@ -136,43 +137,19 @@ bool Motor::check_DRV_fault() { return true; } -void Motor::set_error(Motor::Error_t error){ +void Motor::set_error(Motor::Error error){ error_ |= error; axis_->error_ |= Axis::ERROR_MOTOR_FAILED; safety_critical_disarm_motor_pwm(*this); update_brake_current(); } -float Motor::get_inverter_temp() { - float adc = adc_measurements_[hw_config_.inverter_thermistor_adc_ch]; - float normalized_voltage = adc / adc_full_scale; - return horner_fma(normalized_voltage, thermistor_poly_coeffs, thermistor_num_coeffs); -} - -bool Motor::update_thermal_limits() { - float fet_temp = get_inverter_temp(); - float temp_margin = config_.inverter_temp_limit_upper - fet_temp; - float derating_range = config_.inverter_temp_limit_upper - config_.inverter_temp_limit_lower; - thermal_current_lim_ = config_.current_lim * (temp_margin / derating_range); - if (!(thermal_current_lim_ >= 0.0f)) { //Funny polarity to also catch NaN - thermal_current_lim_ = 0.0f; - } - if (fet_temp > config_.inverter_temp_limit_upper + 5) { - set_error(ERROR_INVERTER_OVER_TEMP); - return false; - } - return true; -} - bool Motor::do_checks() { if (!check_DRV_fault()) { set_error(ERROR_DRV_FAULT); return false; } - if (!update_thermal_limits()) { - //error already set in function - return false; - } + return true; } @@ -181,14 +158,34 @@ float Motor::effective_current_lim() { float current_lim = config_.current_lim; // Hardware limit if (axis_->motor_.config_.motor_type == Motor::MOTOR_TYPE_GIMBAL) { - current_lim = std::min(current_lim, 0.98f*one_by_sqrt3*vbus_voltage); + current_lim = std::min(current_lim, 0.98f*one_by_sqrt3*vbus_voltage); //gimbal motor is voltage control } else { current_lim = std::min(current_lim, axis_->motor_.current_control_.max_allowed_current); } - // Thermal limit - current_lim = std::min(current_lim, thermal_current_lim_); - return current_lim; + // Apply axis current limiters + for (const CurrentLimiter* const limiter : axis_->current_limiters_) { + current_lim = std::min(current_lim, limiter->get_current_limit(config_.current_lim)); + } + + effective_current_lim_ = current_lim; + + return effective_current_lim_; +} + +//return the maximum available torque for the motor. +//Note - for ACIM motors, available torque is allowed to be 0. +float Motor::max_available_torque() { + if (config_.motor_type == Motor::MOTOR_TYPE_ACIM) { + float max_torque = effective_current_lim() * config_.torque_constant * current_control_.acim_rotor_flux; + max_torque = std::clamp(max_torque, 0.0f, config_.torque_lim); + return max_torque; + } + else { + float max_torque = effective_current_lim() * config_.torque_constant; + max_torque = std::clamp(max_torque, 0.0f, config_.torque_lim); + return max_torque; + } } void Motor::log_timing(TimingLog_t log_idx) { @@ -215,7 +212,7 @@ float Motor::phase_current_from_adcval(uint32_t ADCValue) { // TODO check Ibeta balance to verify good motor connection bool Motor::measure_phase_resistance(float test_current, float max_voltage) { static const float kI = 10.0f; // [(V/s)/A] - static const int num_test_cycles = static_cast(3.0f / CURRENT_MEAS_PERIOD); // Test runs for 3s + static const int num_test_cycles = (int)(3.0f / CURRENT_MEAS_PERIOD); // Test runs for 3s float test_voltage = 0.0f; size_t i = 0; @@ -284,7 +281,8 @@ bool Motor::measure_phase_inductance(float voltage_low, float voltage_high) { bool Motor::run_calibration() { float R_calib_max_voltage = config_.resistance_calib_max_voltage; - if (config_.motor_type == MOTOR_TYPE_HIGH_CURRENT) { + if (config_.motor_type == MOTOR_TYPE_HIGH_CURRENT + || config_.motor_type == MOTOR_TYPE_ACIM) { if (!measure_phase_resistance(config_.calibration_current, R_calib_max_voltage)) return false; if (!measure_phase_inductance(-R_calib_max_voltage, R_calib_max_voltage)) @@ -327,7 +325,7 @@ bool Motor::FOC_voltage(float v_d, float v_q, float pwm_phase) { float c = our_arm_cos_f32(pwm_phase); float s = our_arm_sin_f32(pwm_phase); float v_alpha = c*v_d - s*v_q; - float v_beta = c*v_q + s*v_d; + float v_beta = c*v_q + s*v_d; return enqueue_voltage_timings(v_alpha, v_beta); } @@ -339,8 +337,7 @@ bool Motor::FOC_current(float Id_des, float Iq_des, float I_phase, float pwm_pha ictrl.Iq_setpoint = Iq_des; // Check for current sense saturation - if (fabsf(current_meas_.phB) > ictrl.overcurrent_trip_level - || fabsf(current_meas_.phC) > ictrl.overcurrent_trip_level) { + if (std::abs(current_meas_.phB) > ictrl.overcurrent_trip_level || std::abs(current_meas_.phC) > ictrl.overcurrent_trip_level) { set_error(ERROR_CURRENT_SENSE_SATURATION); return false; } @@ -358,9 +355,9 @@ bool Motor::FOC_current(float Id_des, float Iq_des, float I_phase, float pwm_pha ictrl.Id_measured += ictrl.I_measured_report_filter_k * (Id - ictrl.Id_measured); // Check for violation of current limit - float I_trip = config_.current_lim_tolerance * effective_current_lim(); + float I_trip = effective_current_lim() + config_.current_lim_margin; if (SQ(Id) + SQ(Iq) > SQ(I_trip)) { - set_error(ERROR_CURRENT_UNSTABLE); + set_error(ERROR_CURRENT_LIMIT_VIOLATION); return false; } @@ -399,7 +396,7 @@ bool Motor::FOC_current(float Id_des, float Iq_des, float I_phase, float pwm_pha float c_p = our_arm_cos_f32(pwm_phase); float s_p = our_arm_sin_f32(pwm_phase); float mod_alpha = c_p * mod_d - s_p * mod_q; - float mod_beta = c_p * mod_q + s_p * mod_d; + float mod_beta = c_p * mod_q + s_p * mod_d; // Report final applied voltage in stationary frame (for sensorles estimator) ictrl.final_v_alpha = mod_to_V * mod_alpha; @@ -410,30 +407,94 @@ bool Motor::FOC_current(float Id_des, float Iq_des, float I_phase, float pwm_pha return false; // error set inside enqueue_modulation_timings log_timing(TIMING_LOG_FOC_CURRENT); + if (axis_->axis_num_ == 0) { + + // Edit these to suit your capture needs + float trigger_data = ictrl.v_current_control_integral_d; + float trigger_threshold = 0.5f; + float sample_data = Ialpha; + + static bool ready = false; + static bool capturing = false; + if (trigger_data < trigger_threshold) { + ready = true; + } + if (ready && trigger_data >= trigger_threshold) { + capturing = true; + ready = false; + } + if (capturing) { + oscilloscope[oscilloscope_pos] = sample_data; + if (++oscilloscope_pos >= OSCILLOSCOPE_SIZE) { + oscilloscope_pos = 0; + capturing = false; + } + } + } + return true; } - -bool Motor::update(float current_setpoint, float phase, float phase_vel) { - current_setpoint *= config_.direction; +// torque_setpoint [Nm] +// phase [rad electrical] +// phase_vel [rad/s electrical] +bool Motor::update(float torque_setpoint, float phase, float phase_vel) { + float current_setpoint = 0.0f; phase *= config_.direction; phase_vel *= config_.direction; + if (config_.motor_type == MOTOR_TYPE_ACIM) { + current_setpoint = torque_setpoint / (config_.torque_constant * fmax(current_control_.acim_rotor_flux, config_.acim_gain_min_flux)); + } + else { + current_setpoint = torque_setpoint / config_.torque_constant; + } + current_setpoint *= config_.direction; + + // TODO: 2-norm vs independent clamping (current could be sqrt(2) bigger) + float ilim = effective_current_lim(); + float id = std::clamp(current_control_.Id_setpoint, -ilim, ilim); + float iq = std::clamp(current_setpoint, -ilim, ilim); + + if (config_.motor_type == MOTOR_TYPE_ACIM) { + // Note that the effect of the current commands on the real currents is actually 1.5 PWM cycles later + // However the rotor time constant is (usually) so slow that it doesn't matter + // So we elect to write it as if the effect is immediate, to have cleaner code + + if (config_.acim_autoflux_enable) { + float abs_iq = fabsf(iq); + float gain = abs_iq > id ? config_.acim_autoflux_attack_gain : config_.acim_autoflux_decay_gain; + id += gain * (abs_iq - id) * current_meas_period; + id = std::clamp(id, config_.acim_autoflux_min_Id, ilim); + current_control_.Id_setpoint = id; + } + + // acim_rotor_flux is normalized to units of [A] tracking Id; rotor inductance is unspecified + float dflux_by_dt = config_.acim_slip_velocity * (id - current_control_.acim_rotor_flux); + current_control_.acim_rotor_flux += dflux_by_dt * current_meas_period; + float slip_velocity = config_.acim_slip_velocity * (iq / current_control_.acim_rotor_flux); + // Check for issues with small denominator. Polarity of check to catch NaN too + bool acceptable_vel = fabsf(slip_velocity) <= 0.1f * (float)current_meas_hz; + if (!acceptable_vel) + slip_velocity = 0.0f; + phase_vel += slip_velocity; + // reporting only: + current_control_.async_phase_vel = slip_velocity; + + current_control_.async_phase_offset += slip_velocity * current_meas_period; + current_control_.async_phase_offset = wrap_pm_pi(current_control_.async_phase_offset); + phase += current_control_.async_phase_offset; + phase = wrap_pm_pi(phase); + } + float pwm_phase = phase + 1.5f * current_meas_period * phase_vel; // Execute current command - // TODO: move this into the mot - if (config_.motor_type == MOTOR_TYPE_HIGH_CURRENT) { - if(!FOC_current(0.0f, current_setpoint, phase, pwm_phase)){ - return false; - } - } else if (config_.motor_type == MOTOR_TYPE_GIMBAL) { - //In gimbal motor mode, current is reinterptreted as voltage. - if(!FOC_voltage(0.0f, current_setpoint, pwm_phase)) - return false; - } else { - set_error(ERROR_NOT_IMPLEMENTED_MOTOR_TYPE); - return false; + switch(config_.motor_type){ + case MOTOR_TYPE_HIGH_CURRENT: return FOC_current(id, iq, phase, pwm_phase); break; + case MOTOR_TYPE_ACIM: return FOC_current(id, iq, phase, pwm_phase); break; + case MOTOR_TYPE_GIMBAL: return FOC_voltage(id, iq, pwm_phase); break; + default: set_error(ERROR_NOT_IMPLEMENTED_MOTOR_TYPE); return false; break; } return true; } diff --git a/Firmware/MotorControl/motor.hpp b/Firmware/MotorControl/motor.hpp index d31985ee..b8fdcc33 100644 --- a/Firmware/MotorControl/motor.hpp +++ b/Firmware/MotorControl/motor.hpp @@ -7,31 +7,8 @@ #include "drv8301.h" -class Motor { +class Motor : public ODriveIntf::MotorIntf { public: - enum Error_t { - ERROR_NONE = 0, - ERROR_PHASE_RESISTANCE_OUT_OF_RANGE = 0x0001, - ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE = 0x0002, - ERROR_ADC_FAILED = 0x0004, - ERROR_DRV_FAULT = 0x0008, - ERROR_CONTROL_DEADLINE_MISSED = 0x0010, - ERROR_NOT_IMPLEMENTED_MOTOR_TYPE = 0x0020, - ERROR_BRAKE_CURRENT_OUT_OF_RANGE = 0x0040, - ERROR_MODULATION_MAGNITUDE = 0x0080, - ERROR_BRAKE_DEADTIME_VIOLATION = 0x0100, - ERROR_UNEXPECTED_TIMER_CALLBACK = 0x0200, - ERROR_CURRENT_SENSE_SATURATION = 0x0400, - ERROR_INVERTER_OVER_TEMP = 0x0800, - ERROR_CURRENT_UNSTABLE = 0x1000 - }; - - enum MotorType_t { - MOTOR_TYPE_HIGH_CURRENT = 0, - // MOTOR_TYPE_LOW_CURRENT = 1, //Not yet implemented - MOTOR_TYPE_GIMBAL = 2 - }; - struct Iph_BC_t { float phB; float phC; @@ -46,16 +23,20 @@ public: // Voltage applied at end of cycle: float final_v_alpha; // [V] float final_v_beta; // [V] + float Id_setpoint; // [A] float Iq_setpoint; // [A] float Iq_measured; // [A] float Id_measured; // [A] float I_measured_report_filter_k; float max_allowed_current; // [A] float overcurrent_trip_level; // [A] + float acim_rotor_flux; // [A] + float async_phase_vel; // [rad/s electrical] + float async_phase_offset; // [rad electrical] }; - // NOTE: for gimbal motors, all units of A are instead V. - // example: vel_gain is [V/(count/s)] instead of [A/(count/s)] + // NOTE: for gimbal motors, all units of Nm are instead V. + // example: vel_gain is [V/(turn/s)] instead of [Nm/(turn/s)] // example: current_lim and calibration_current will instead determine the maximum voltage applied to the motor. struct Config_t { bool pre_calibrated = false; // can be set to true to indicate that all values here are valid @@ -64,37 +45,35 @@ public: float resistance_calib_max_voltage = 2.0f; // [V] - You may need to increase this if this voltage isn't sufficient to drive calibration_current through the motor. float phase_inductance = 0.0f; // to be set by measure_phase_inductance float phase_resistance = 0.0f; // to be set by measure_phase_resistance + float torque_constant = 0.04f; // [Nm/A] for PM motors, [Nm/A^2] for induction motors. Equal to 8.27/Kv of the motor int32_t direction = 0; // 1 or -1 (0 = unspecified) - MotorType_t motor_type = MOTOR_TYPE_HIGH_CURRENT; + MotorType motor_type = MOTOR_TYPE_HIGH_CURRENT; // Read out max_allowed_current to see max supported value for current_lim. // float current_lim = 70.0f; //[A] - float current_lim = 10.0f; //[A] - float current_lim_tolerance = 1.25f; // multiple of current_lim + float current_lim = 10.0f; //[A] + float current_lim_margin = 8.0f; // Maximum violation of current_lim + float torque_lim = std::numeric_limits::infinity(); //[Nm]. // Value used to compute shunt amplifier gains float requested_current_range = 60.0f; // [A] float current_control_bandwidth = 1000.0f; // [rad/s] float inverter_temp_limit_lower = 100; float inverter_temp_limit_upper = 120; - }; + float acim_slip_velocity = 14.706f; // [rad/s electrical] = 1/rotor_tau + float acim_gain_min_flux = 10; // [A] + float acim_autoflux_min_Id = 10; // [A] + bool acim_autoflux_enable = false; + float acim_autoflux_attack_gain = 10.0f; + float acim_autoflux_decay_gain = 1.0f; - enum TimingLog_t { - TIMING_LOG_GENERAL, - TIMING_LOG_ADC_CB_I, - TIMING_LOG_ADC_CB_DC, - TIMING_LOG_MEAS_R, - TIMING_LOG_MEAS_L, - TIMING_LOG_ENC_CALIB, - TIMING_LOG_IDX_SEARCH, - TIMING_LOG_FOC_VOLTAGE, - TIMING_LOG_FOC_CURRENT, - TIMING_LOG_NUM_SLOTS - }; - - enum ArmedState_t { - ARMED_STATE_DISARMED, - ARMED_STATE_WAITING_FOR_TIMINGS, - ARMED_STATE_WAITING_FOR_UPDATE, - ARMED_STATE_ARMED, + // custom property setters + Motor* parent = nullptr; + void set_pre_calibrated(bool value) { + pre_calibrated = value; + parent->is_calibrated_ = parent->is_calibrated_ || parent->config_.pre_calibrated; + } + void set_phase_inductance(float value) { phase_inductance = value; parent->update_current_controller_gains(); } + void set_phase_resistance(float value) { phase_resistance = value; parent->update_current_controller_gains(); } + void set_current_control_bandwidth(float value) { current_control_bandwidth = value; parent->update_current_controller_gains(); } }; Motor(const MotorHardwareConfig_t& hw_config, @@ -111,11 +90,10 @@ public: void update_current_controller_gains(); void DRV8301_setup(); bool check_DRV_fault(); - void set_error(Error_t error); + void set_error(Error error); bool do_checks(); - float get_inverter_temp(); - bool update_thermal_limits(); float effective_current_lim(); + float max_available_torque(); void log_timing(TimingLog_t log_idx); float phase_current_from_adcval(uint32_t ADCValue); bool measure_phase_resistance(float test_current, float max_voltage); @@ -143,13 +121,17 @@ public: bool next_timings_valid_ = false; uint16_t last_cpu_time_ = 0; int timing_log_index_ = 0; - uint16_t timing_log_[TIMING_LOG_NUM_SLOTS] = { 0 }; + struct { + uint16_t& operator[](size_t idx) { return content[idx]; } + uint16_t& get(size_t idx) { return content[idx]; } + uint16_t content[TIMING_LOG_NUM_SLOTS]; + } timing_log_; // variables exposed on protocol - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; // Do not write to this variable directly! // It is for exclusive use by the safety_critical_... functions. - ArmedState_t armed_state_ = ARMED_STATE_DISARMED; + ArmedState armed_state_ = ARMED_STATE_DISARMED; bool is_calibrated_ = config_.pre_calibrated; Iph_BC_t current_meas_ = {0.0f, 0.0f}; Iph_BC_t DC_calib_ = {0.0f, 0.0f}; @@ -162,84 +144,22 @@ public: .Ibus = 0.0f, .final_v_alpha = 0.0f, .final_v_beta = 0.0f, + .Id_setpoint = 0.0f, .Iq_setpoint = 0.0f, .Iq_measured = 0.0f, .Id_measured = 0.0f, .I_measured_report_filter_k = 1.0f, .max_allowed_current = 0.0f, .overcurrent_trip_level = 0.0f, + .acim_rotor_flux = 0.0f, + .async_phase_vel = 0.0f, + .async_phase_offset = 0.0f, }; - DRV8301_FaultType_e drv_fault_ = DRV8301_FaultType_NoFault; + struct : GateDriverIntf { + DrvFault drv_fault = DRV_FAULT_NO_FAULT; + } gate_driver_exported_; DRV_SPI_8301_Vars_t gate_driver_regs_; //Local view of DRV registers (initialized by DRV8301_setup) - float thermal_current_lim_ = 10.0f; //[A] - - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_ro_property("armed_state", &armed_state_), - make_protocol_ro_property("is_calibrated", &is_calibrated_), - make_protocol_ro_property("current_meas_phB", ¤t_meas_.phB), - make_protocol_ro_property("current_meas_phC", ¤t_meas_.phC), - make_protocol_property("DC_calib_phB", &DC_calib_.phB), - make_protocol_property("DC_calib_phC", &DC_calib_.phC), - make_protocol_property("phase_current_rev_gain", &phase_current_rev_gain_), - make_protocol_ro_property("thermal_current_lim", &thermal_current_lim_), - make_protocol_function("get_inverter_temp", *this, &Motor::get_inverter_temp), - make_protocol_object("current_control", - make_protocol_property("p_gain", ¤t_control_.p_gain), - make_protocol_property("i_gain", ¤t_control_.i_gain), - make_protocol_property("v_current_control_integral_d", ¤t_control_.v_current_control_integral_d), - make_protocol_property("v_current_control_integral_q", ¤t_control_.v_current_control_integral_q), - make_protocol_property("Ibus", ¤t_control_.Ibus), - make_protocol_property("final_v_alpha", ¤t_control_.final_v_alpha), - make_protocol_property("final_v_beta", ¤t_control_.final_v_beta), - make_protocol_property("Iq_setpoint", ¤t_control_.Iq_setpoint), - make_protocol_property("Iq_measured", ¤t_control_.Iq_measured), - make_protocol_property("Id_measured", ¤t_control_.Id_measured), - make_protocol_property("I_measured_report_filter_k", ¤t_control_.I_measured_report_filter_k), - make_protocol_ro_property("max_allowed_current", ¤t_control_.max_allowed_current), - make_protocol_ro_property("overcurrent_trip_level", ¤t_control_.overcurrent_trip_level) - ), - make_protocol_object("gate_driver", - make_protocol_ro_property("drv_fault", &drv_fault_) - // make_protocol_ro_property("status_reg_1", &gate_driver_regs_.Stat_Reg_1_Value), - // make_protocol_ro_property("status_reg_2", &gate_driver_regs_.Stat_Reg_2_Value), - // make_protocol_ro_property("ctrl_reg_1", &gate_driver_regs_.Ctrl_Reg_1_Value), - // make_protocol_ro_property("ctrl_reg_2", &gate_driver_regs_.Ctrl_Reg_2_Value) - ), - make_protocol_object("timing_log", - make_protocol_ro_property("TIMING_LOG_GENERAL", &timing_log_[TIMING_LOG_GENERAL]), - make_protocol_ro_property("TIMING_LOG_ADC_CB_I", &timing_log_[TIMING_LOG_ADC_CB_I]), - make_protocol_ro_property("TIMING_LOG_ADC_CB_DC", &timing_log_[TIMING_LOG_ADC_CB_DC]), - make_protocol_ro_property("TIMING_LOG_MEAS_R", &timing_log_[TIMING_LOG_MEAS_R]), - make_protocol_ro_property("TIMING_LOG_MEAS_L", &timing_log_[TIMING_LOG_MEAS_L]), - make_protocol_ro_property("TIMING_LOG_ENC_CALIB", &timing_log_[TIMING_LOG_ENC_CALIB]), - make_protocol_ro_property("TIMING_LOG_IDX_SEARCH", &timing_log_[TIMING_LOG_IDX_SEARCH]), - make_protocol_ro_property("TIMING_LOG_FOC_VOLTAGE", &timing_log_[TIMING_LOG_FOC_VOLTAGE]), - make_protocol_ro_property("TIMING_LOG_FOC_CURRENT", &timing_log_[TIMING_LOG_FOC_CURRENT]) - ), - make_protocol_object("config", - make_protocol_property("pre_calibrated", &config_.pre_calibrated), - make_protocol_property("pole_pairs", &config_.pole_pairs), - make_protocol_property("calibration_current", &config_.calibration_current), - make_protocol_property("resistance_calib_max_voltage", &config_.resistance_calib_max_voltage), - make_protocol_property("phase_inductance", &config_.phase_inductance), - make_protocol_property("phase_resistance", &config_.phase_resistance), - make_protocol_property("direction", &config_.direction), - make_protocol_property("motor_type", &config_.motor_type), - make_protocol_property("current_lim", &config_.current_lim), - make_protocol_property("current_lim_tolerance", &config_.current_lim_tolerance), - make_protocol_property("inverter_temp_limit_lower", &config_.inverter_temp_limit_lower), - make_protocol_property("inverter_temp_limit_upper", &config_.inverter_temp_limit_upper), - make_protocol_property("requested_current_range", &config_.requested_current_range), - make_protocol_property("current_control_bandwidth", &config_.current_control_bandwidth, - [](void* ctx) { static_cast(ctx)->update_current_controller_gains(); }, this) - ) - ); - } + float effective_current_lim_ = 10.0f; }; -DEFINE_ENUM_FLAG_OPERATORS(Motor::Error_t) - #endif // __MOTOR_HPP diff --git a/Firmware/MotorControl/nvm.c b/Firmware/MotorControl/nvm.c index e581beee..5a1b4a71 100644 --- a/Firmware/MotorControl/nvm.c +++ b/Firmware/MotorControl/nvm.c @@ -362,10 +362,12 @@ int NVM_commit(void) { read_sector_ = 1 - read_sector_; // invalidate the other sector - if (read_sector->index < read_sector->n_data) + if (read_sector->index < read_sector->n_data) { status = set_allocation_state(read_sector, read_sector->index, 1, INVALID); - else + read_sector->index += 1; + } else { status = erase(read_sector); + } return status; } diff --git a/Firmware/MotorControl/odrive_main.h b/Firmware/MotorControl/odrive_main.h index 677fb996..392b9c7e 100644 --- a/Firmware/MotorControl/odrive_main.h +++ b/Firmware/MotorControl/odrive_main.h @@ -10,6 +10,8 @@ #ifdef __cplusplus #include +#include +#include extern "C" { #endif @@ -33,16 +35,21 @@ extern "C" { //default timeout waiting for phase measurement signals #define PH_CURRENT_MEAS_TIMEOUT 2 // [ms] -//TODO clean this up +// Period in [s] static const float current_meas_period = CURRENT_MEAS_PERIOD; + +// Frequency in [Hz] static const int current_meas_hz = CURRENT_MEAS_HZ; + // extern const float elec_rad_per_enc; extern uint32_t _reboot_cookie; -extern bool user_config_loaded_; extern uint64_t serial_number; extern char serial_number_str[13]; +#ifdef __cplusplus +} + typedef struct { bool fully_booted; uint32_t uptime; // [ms] @@ -54,14 +61,23 @@ typedef struct { uint32_t min_stack_space_uart; uint32_t min_stack_space_usb_irq; uint32_t min_stack_space_startup; -} SystemStats_t; -extern SystemStats_t system_stats_; + uint32_t min_stack_space_can; -#ifdef __cplusplus -} + uint32_t stack_usage_axis0; + uint32_t stack_usage_axis1; + uint32_t stack_usage_comms; + uint32_t stack_usage_usb; + uint32_t stack_usage_uart; + uint32_t stack_usage_usb_irq; + uint32_t stack_usage_startup; + uint32_t stack_usage_can; + + USBStats_t& usb = usb_stats_; + I2CStats_t& i2c = i2c_stats_; +} SystemStats_t; struct PWMMapping_t { - endpoint_ref_t endpoint = { 0 }; + endpoint_ref_t endpoint; float min = 0; float max = 0; }; @@ -71,6 +87,7 @@ struct BoardConfig_t { bool enable_uart = true; bool enable_i2c_instead_of_can = false; bool enable_ascii_protocol_on_usb = true; + float max_regen_current = 0.0f; #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 5 && HW_VERSION_VOLTAGE >= 48 float brake_resistance = 2.0f; // [ohm] #else @@ -81,20 +98,71 @@ struct BoardConfig_t { // brake_duty_cycle += 0% + * vbus_voltage == dc_bus_overvoltage_ramp_end => brake_duty_cycle += 100% + * + * Remarks: + * - This feature is active even when all motors are disarmed. + * - This feature is disabled if `brake_resistance` is non-positive. + */ + bool enable_dc_bus_overvoltage_ramp = false; + float dc_bus_overvoltage_ramp_start = 1.07f * HW_VERSION_VOLTAGE; //!< See `enable_dc_bus_overvoltage_ramp`. + //!< Do not set this lower than your usual vbus_voltage, + //!< unless you like fried brake resistors. + float dc_bus_overvoltage_ramp_end = 1.07f * HW_VERSION_VOLTAGE; //!< See `enable_dc_bus_overvoltage_ramp`. + //!< Must be larger than `dc_bus_overvoltage_ramp_start`, + //!< otherwise the ramp feature is disabled. + + float dc_max_positive_current = INFINITY; // Max current [A] the power supply can source + float dc_max_negative_current = -0.000001f; // Max current [A] the power supply can sink. You most likely want a non-positive value here. Set to -INFINITY to disable. PWMMapping_t pwm_mappings[GPIO_COUNT]; PWMMapping_t analog_mappings[GPIO_COUNT]; -}; -extern BoardConfig_t board_config; -extern bool user_config_loaded_; + /** + * Defines the baudrate used on the UART interface. + * Some baudrates will have a small timing error due to hardware limitations. + * + * Here's an (incomplete) list of baudrates for ODrive v3.x: + * + * Configured | Actual | Error [%] + * -------------|---------------|----------- + * 1.2 KBps | 1.2 KBps | 0 + * 2.4 KBps | 2.4 KBps | 0 + * 9.6 KBps | 9.6 KBps | 0 + * 19.2 KBps | 19.195 KBps | 0.02 + * 38.4 KBps | 38.391 KBps | 0.02 + * 57.6 KBps | 57.613 KBps | 0.02 + * 115.2 KBps | 115.068 KBps | 0.11 + * 230.4 KBps | 230.769 KBps | 0.16 + * 460.8 KBps | 461.538 KBps | 0.16 + * 921.6 KBps | 913.043 KBps | 0.93 + * 1.792 MBps | 1.826 MBps | 1.9 + * 1.8432 MBps | 1.826 MBps | 0.93 + * + * For more information refer to Section 30.3.4 and Table 142 (the column with f_PCLK = 42 MHz) in the STM datasheet: + * https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf + */ + uint32_t uart_baudrate = 115200; +}; + +// Forward Declarations class Axis; class Motor; +class ODriveCAN; constexpr size_t AXIS_COUNT = 2; -extern Axis *axes[AXIS_COUNT]; +extern std::array axes; +extern ODriveCAN *odCAN; // if you use the oscilloscope feature you can bump up this value -#define OSCILLOSCOPE_SIZE 128 +#define OSCILLOSCOPE_SIZE 4096 extern float oscilloscope[OSCILLOSCOPE_SIZE]; extern size_t oscilloscope_pos; @@ -110,23 +178,120 @@ inline ENUMTYPE &operator ^= (ENUMTYPE &a, ENUMTYPE b) { return reinterpret_cast inline ENUMTYPE operator ~ (ENUMTYPE a) { return static_cast(~static_cast>(a)); } +enum TimingLog_t { + TIMING_LOG_GENERAL, + TIMING_LOG_ADC_CB_I, + TIMING_LOG_ADC_CB_DC, + TIMING_LOG_MEAS_R, + TIMING_LOG_MEAS_L, + TIMING_LOG_ENC_CALIB, + TIMING_LOG_IDX_SEARCH, + TIMING_LOG_FOC_VOLTAGE, + TIMING_LOG_FOC_CURRENT, + TIMING_LOG_SPI_START, + TIMING_LOG_SAMPLE_NOW, + TIMING_LOG_SPI_END, + TIMING_LOG_NUM_SLOTS +}; + + +#include "autogen/interfaces.hpp" + // ODrive specific includes -#include +#include +#include #include +#include #include #include #include -#include +#include +#include #include +#include #include #include -#endif // __cplusplus +// Defined in autogen/version.c based on git-derived version numbers +extern "C" { +extern const unsigned char fw_version_major_; +extern const unsigned char fw_version_minor_; +extern const unsigned char fw_version_revision_; +extern const unsigned char fw_version_unreleased_; +} // general system functions defined in main.cpp -void save_configuration(void); -void erase_configuration(void); -void enter_dfu_mode(void); +class ODrive : public ODriveIntf { +public: + void save_configuration() override; + void erase_configuration() override; + void reboot() override { NVIC_SystemReset(); } + void enter_dfu_mode() override; + + float get_oscilloscope_val(uint32_t index) override { + return oscilloscope[index]; + } + + float get_adc_voltage(uint32_t gpio) override { + return ::get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)); + } + + int32_t test_function(int32_t delta) override { + static int cnt = 0; + return cnt += delta; + } + + Axis& get_axis(int num) { return *axes[num]; } + ODriveCAN& get_can() { return *odCAN; } + + float& vbus_voltage_ = ::vbus_voltage; // TODO: make this the actual variable + float& ibus_ = ::ibus_; // TODO: make this the actual variable + float ibus_report_filter_k_ = 1.0f; + + const uint64_t& serial_number_ = ::serial_number; + +#if HW_VERSION_MAJOR == 3 + // Determine start address of the OTP struct: + // The OTP is organized into 16-byte blocks. + // If the first block starts with "0xfe" we use the first block. + // If the first block starts with "0x00" and the second block starts with "0xfe", + // we use the second block. This gives the user the chance to screw up once. + // If none of the above is the case, we consider the OTP invalid (otp_ptr will be NULL). + const uint8_t* otp_ptr = + (*(uint8_t*)FLASH_OTP_BASE == 0xfe) ? (uint8_t*)FLASH_OTP_BASE : + (*(uint8_t*)FLASH_OTP_BASE != 0x00) ? NULL : + (*(uint8_t*)(FLASH_OTP_BASE + 0x10) != 0xfe) ? NULL : + (uint8_t*)(FLASH_OTP_BASE + 0x10); + + // Read hardware version from OTP if available, otherwise fall back + // to software defined version. + const uint8_t hw_version_major_ = otp_ptr ? otp_ptr[3] : HW_VERSION_MAJOR; + const uint8_t hw_version_minor_ = otp_ptr ? otp_ptr[4] : HW_VERSION_MINOR; + const uint8_t hw_version_variant_ = otp_ptr ? otp_ptr[5] : HW_VERSION_VOLTAGE; +#else +#error "not implemented" +#endif + + // the corresponding macros are defined in the autogenerated version.h + const uint8_t fw_version_major_ = ::fw_version_major_; + const uint8_t fw_version_minor_ = ::fw_version_minor_; + const uint8_t fw_version_revision_ = ::fw_version_revision_; + const uint8_t fw_version_unreleased_ = ::fw_version_unreleased_; // 0 for official releases, 1 otherwise + + bool& brake_resistor_armed_ = ::brake_resistor_armed; // TODO: make this the actual variable + bool& brake_resistor_saturated_ = ::brake_resistor_saturated; // TODO: make this the actual variable + + SystemStats_t system_stats_; + + BoardConfig_t config_; + bool user_config_loaded_; + + uint32_t test_property_ = 0; +}; + +extern ODrive odrv; // defined in main.cpp + +#endif // __cplusplus #endif /* __ODRIVE_MAIN_H */ diff --git a/Firmware/MotorControl/sensorless_estimator.cpp b/Firmware/MotorControl/sensorless_estimator.cpp index 43191ce3..aebbc09b 100644 --- a/Firmware/MotorControl/sensorless_estimator.cpp +++ b/Firmware/MotorControl/sensorless_estimator.cpp @@ -65,6 +65,7 @@ bool SensorlessEstimator::update() { // Check that we don't get problems with discrete time approximation if (!(current_meas_period * pll_kp < 1.0f)) { error_ |= ERROR_UNSTABLE_GAIN; + vel_estimate_valid_ = false; return false; } @@ -77,5 +78,6 @@ bool SensorlessEstimator::update() { // update PLL velocity vel_estimate_ += current_meas_period * pll_ki * delta_phase; + vel_estimate_valid_ = true; return true; }; diff --git a/Firmware/MotorControl/sensorless_estimator.hpp b/Firmware/MotorControl/sensorless_estimator.hpp index 719a3227..95992ae0 100644 --- a/Firmware/MotorControl/sensorless_estimator.hpp +++ b/Firmware/MotorControl/sensorless_estimator.hpp @@ -1,13 +1,8 @@ #ifndef __SENSORLESS_ESTIMATOR_HPP #define __SENSORLESS_ESTIMATOR_HPP -class SensorlessEstimator { +class SensorlessEstimator : public ODriveIntf::SensorlessEstimatorIntf { public: - enum Error_t { - ERROR_NONE = 0, - ERROR_UNSTABLE_GAIN = 0x01, - }; - struct Config_t { float observer_gain = 1000.0f; // [rad/s] float pll_bandwidth = 1000.0f; // [rad/s] @@ -22,34 +17,16 @@ public: Config_t& config_; // TODO: expose on protocol - Error_t error_ = ERROR_NONE; + Error error_ = ERROR_NONE; float phase_ = 0.0f; // [rad] float pll_pos_ = 0.0f; // [rad] float vel_estimate_ = 0.0f; // [rad/s] + bool vel_estimate_valid_ = false; // float pll_kp_ = 0.0f; // [rad/s / rad] // float pll_ki_ = 0.0f; // [(rad/s^2) / rad] float flux_state_[2] = {0.0f, 0.0f}; // [Vs] float V_alpha_beta_memory_[2] = {0.0f, 0.0f}; // [V] bool estimator_good_ = false; - - // Communication protocol definitions - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_property("error", &error_), - make_protocol_property("phase", &phase_), - make_protocol_property("pll_pos", &pll_pos_), - make_protocol_property("vel_estimate", &vel_estimate_), - // make_protocol_property("pll_kp", &pll_kp_), - // make_protocol_property("pll_ki", &pll_ki_), - make_protocol_object("config", - make_protocol_property("observer_gain", &config_.observer_gain), - make_protocol_property("pll_bandwidth", &config_.pll_bandwidth), - make_protocol_property("pm_flux_linkage", &config_.pm_flux_linkage) - ) - ); - } }; -DEFINE_ENUM_FLAG_OPERATORS(SensorlessEstimator::Error_t) - #endif /* __SENSORLESS_ESTIMATOR_HPP */ diff --git a/Firmware/MotorControl/thermistor.cpp b/Firmware/MotorControl/thermistor.cpp new file mode 100644 index 00000000..68656315 --- /dev/null +++ b/Firmware/MotorControl/thermistor.cpp @@ -0,0 +1,80 @@ +#include "odrive_main.h" + +#include "low_level.h" + +ThermistorCurrentLimiter::ThermistorCurrentLimiter(uint16_t adc_channel, + const float* const coefficients, + size_t num_coeffs, + const float& temp_limit_lower, + const float& temp_limit_upper, + const bool& enabled) : + adc_channel_(adc_channel), + coefficients_(coefficients), + num_coeffs_(num_coeffs), + temperature_(NAN), + temp_limit_lower_(temp_limit_lower), + temp_limit_upper_(temp_limit_upper), + enabled_(enabled), + error_(ERROR_NONE) +{ +} + +void ThermistorCurrentLimiter::update() { + const float voltage = get_adc_voltage_channel(adc_channel_); + const float normalized_voltage = voltage / adc_ref_voltage; + temperature_ = horner_fma(normalized_voltage, coefficients_, num_coeffs_); +} + +bool ThermistorCurrentLimiter::do_checks() { + if (enabled_ && temperature_ >= temp_limit_upper_ + 5) { + error_ = ERROR_OVER_TEMP; + axis_->error_ |= Axis::ERROR_OVER_TEMP; + return false; + } + return true; +} + +float ThermistorCurrentLimiter::get_current_limit(float base_current_lim) const { + if (!enabled_) { + return base_current_lim; + } + + const float temp_margin = temp_limit_upper_ - temperature_; + const float derating_range = temp_limit_upper_ - temp_limit_lower_; + float thermal_current_lim = base_current_lim * (temp_margin / derating_range); + if (!(thermal_current_lim >= 0.0f)) { // Funny polarity to also catch NaN + thermal_current_lim = 0.0f; + } + + return std::min(thermal_current_lim, base_current_lim); +} + +OnboardThermistorCurrentLimiter::OnboardThermistorCurrentLimiter(const ThermistorHardwareConfig_t& hw_config, Config_t& config) : + ThermistorCurrentLimiter(hw_config.adc_ch, + hw_config.coeffs, + hw_config.num_coeffs, + config.temp_limit_lower, + config.temp_limit_upper, + config.enabled), + config_(config) +{ +} + +OffboardThermistorCurrentLimiter::OffboardThermistorCurrentLimiter(Config_t& config) : + ThermistorCurrentLimiter(UINT16_MAX, + &config.thermistor_poly_coeffs[0], + num_coeffs_, + config.temp_limit_lower, + config.temp_limit_upper, + config.enabled), + config_(config) +{ + decode_pin(); +} + +void OffboardThermistorCurrentLimiter::decode_pin() { + const GPIO_TypeDef* const port = get_gpio_port_by_pin(config_.gpio_pin); + const uint16_t pin = get_gpio_pin_by_pin(config_.gpio_pin); + + adc_channel_ = channel_from_gpio(port, pin); +} diff --git a/Firmware/MotorControl/thermistor.hpp b/Firmware/MotorControl/thermistor.hpp new file mode 100644 index 00000000..c403f189 --- /dev/null +++ b/Firmware/MotorControl/thermistor.hpp @@ -0,0 +1,74 @@ +#ifndef __THERMISTOR_HPP +#define __THERMISTOR_HPP + +#ifndef __ODRIVE_MAIN_H +#error "This file should not be included directly. Include odrive_main.h instead." +#endif + +class ThermistorCurrentLimiter : public CurrentLimiter, public ODriveIntf::ThermistorCurrentLimiterIntf { +public: + virtual ~ThermistorCurrentLimiter() = default; + + ThermistorCurrentLimiter(uint16_t adc_channel, + const float* const coefficients, + size_t num_coeffs, + const float& temp_limit_lower, + const float& temp_limit_upper, + const bool& enabled); + + void update(); + bool do_checks(); + float get_current_limit(float base_current_lim) const override; + + uint16_t adc_channel_; + const float* const coefficients_; + const size_t num_coeffs_; + float temperature_; + const float& temp_limit_lower_; + const float& temp_limit_upper_; + const bool& enabled_; + Error error_; + Axis* axis_ = nullptr; // set by Axis constructor +}; + +class OnboardThermistorCurrentLimiter : public ThermistorCurrentLimiter, public ODriveIntf::OnboardThermistorCurrentLimiterIntf { +public: + struct Config_t { + float temp_limit_lower = 100; + float temp_limit_upper = 120; + bool enabled = true; + }; + + virtual ~OnboardThermistorCurrentLimiter() = default; + OnboardThermistorCurrentLimiter(const ThermistorHardwareConfig_t& hw_config, Config_t& config); + + Config_t& config_; +}; + +class OffboardThermistorCurrentLimiter : public ThermistorCurrentLimiter, public ODriveIntf::OffboardThermistorCurrentLimiterIntf { +public: + static const size_t num_coeffs_ = 4; + + struct Config_t { + float thermistor_poly_coeffs[num_coeffs_]; + + uint16_t gpio_pin = 4; + float temp_limit_lower = 100; + float temp_limit_upper = 120; + bool enabled = false; + + // custom setters + OffboardThermistorCurrentLimiter* parent; + void set_gpio_pin(uint16_t value) { gpio_pin = value; parent->decode_pin(); } + }; + + virtual ~OffboardThermistorCurrentLimiter() = default; + OffboardThermistorCurrentLimiter(Config_t& config); + + Config_t& config_; + +private: + void decode_pin(); +}; + +#endif // __THERMISTOR_HPP diff --git a/Firmware/MotorControl/timer.hpp b/Firmware/MotorControl/timer.hpp new file mode 100644 index 00000000..ae539bb8 --- /dev/null +++ b/Firmware/MotorControl/timer.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +template +class Timer { + public: + void setTimeout(const T timeout) { + timeout_ = timeout; + } + + void setIncrement(const T increment) { + increment_ = increment; + } + + void start() { + running_ = true; + } + + void stop() { + running_ = false; + } + + // If the timer is started, increment the timer + void update() { + if (running_) + timer_ = std::min(timer_ + increment_, timeout_); + } + + void reset() { + timer_ = static_cast(0); + } + + bool expired() { + return timer_ >= timeout_; + } + + private: + T timer_ = static_cast(0); // Current state + T timeout_ = static_cast(0); // Time to count + T increment_ = static_cast(0); // Amount to increment each time update() is called + bool running_ = false; // update() only increments if runing_ is true +}; diff --git a/Firmware/MotorControl/trapTraj.cpp b/Firmware/MotorControl/trapTraj.cpp index f1e41aa5..dd7f64a2 100644 --- a/Firmware/MotorControl/trapTraj.cpp +++ b/Firmware/MotorControl/trapTraj.cpp @@ -1,6 +1,6 @@ #include #include "odrive_main.h" -#include "utils.h" +#include "utils.hpp" // A sign function where input 0 has positive sign (not 0) float sign_hard(float val) { @@ -44,7 +44,7 @@ bool TrapezoidalTrajectory::planTrapezoidal(float Xf, float Xi, float Vi, // Are we displacing enough to reach cruising speed? if (s*dX < s*dXmin) { // Short move (triangle profile) - Vr_ = s * sqrtf((Dr_*SQ(Vi) + 2*Ar_*Dr_*dX) / (Dr_ - Ar_)); + Vr_ = s * sqrtf(std::fmax((Dr_*SQ(Vi) + 2*Ar_*Dr_*dX) / (Dr_ - Ar_), 0.0f)); Ta_ = std::max(0.0f, (Vr_ - Vi) / Ar_); Td_ = std::max(0.0f, -Vr_ / Dr_); Tv_ = 0.0f; diff --git a/Firmware/MotorControl/trapTraj.hpp b/Firmware/MotorControl/trapTraj.hpp index fe5f3fec..9fa5ae33 100644 --- a/Firmware/MotorControl/trapTraj.hpp +++ b/Firmware/MotorControl/trapTraj.hpp @@ -4,10 +4,9 @@ class TrapezoidalTrajectory { public: struct Config_t { - float vel_limit = 20000.0f; // [count/s] - float accel_limit = 5000.0f; // [count/s^2] - float decel_limit = 5000.0f; // [count/s^2] - float A_per_css = 0.0f; // [A/(count/s^2)] + float vel_limit = 2.0f; // [turn/s] + float accel_limit = 0.5f; // [turn/s^2] + float decel_limit = 0.5f; // [turn/s^2] }; struct Step_t { @@ -21,17 +20,6 @@ public: float Vmax, float Amax, float Dmax); Step_t eval(float t); - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_object("config", - make_protocol_property("vel_limit", &config_.vel_limit), - make_protocol_property("accel_limit", &config_.accel_limit), - make_protocol_property("decel_limit", &config_.decel_limit), - make_protocol_property("A_per_css", &config_.A_per_css) - ) - ); - } - Axis* axis_ = nullptr; // set by Axis constructor Config_t& config_; @@ -49,6 +37,8 @@ public: float Tf_; float yAccel_; + + float t_; }; #endif \ No newline at end of file diff --git a/Firmware/MotorControl/utils.c b/Firmware/MotorControl/utils.cpp similarity index 98% rename from Firmware/MotorControl/utils.c rename to Firmware/MotorControl/utils.cpp index 3278d614..579a47a7 100644 --- a/Firmware/MotorControl/utils.c +++ b/Firmware/MotorControl/utils.cpp @@ -1,5 +1,5 @@ -#include +#include #include #include #include @@ -151,7 +151,7 @@ float fast_atan2(float y, float x) { // p(x) = coeffs[0] * x^deg + ... + coeffs[deg], for some degree "deg" float horner_fma(float x, const float *coeffs, size_t count) { float result = 0.0f; - for (int idx = 0; idx < count; ++idx) + for (size_t idx = 0; idx < count; ++idx) result = fmaf(result, x, coeffs[idx]); return result; } diff --git a/Firmware/MotorControl/utils.h b/Firmware/MotorControl/utils.hpp similarity index 79% rename from Firmware/MotorControl/utils.h rename to Firmware/MotorControl/utils.hpp index 3145c19a..49f9434d 100644 --- a/Firmware/MotorControl/utils.h +++ b/Firmware/MotorControl/utils.hpp @@ -2,10 +2,6 @@ #ifndef __UTILS_H #define __UTILS_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -58,29 +54,36 @@ extern "C" { #ifdef M_PI #undef M_PI #endif -#define M_PI 3.14159265358979323846f +#define M_PI (3.14159265358979323846f) #define MACRO_MAX(x, y) (((x) > (y)) ? (x) : (y)) #define MACRO_MIN(x, y) (((x) < (y)) ? (x) : (y)) #define SQ(x) ((x) * (x)) +#ifdef __cplusplus + +#include + +/** + * @brief Small helper to make array with known size + * in contrast to initializer lists the number of arguments + * has to match exactly. Whereas initializer lists allow + * less arguments. + */ +template +std::array make_array(T head, Tail... tail) +{ + return std::array({ head, tail ... }); +} + +extern "C" { +#endif + static const float one_by_sqrt3 = 0.57735026919f; static const float two_by_sqrt3 = 1.15470053838f; static const float sqrt3_by_2 = 0.86602540378f; -//beware of inserting large values! -static inline float wrap_pm(float x, float pm_range) { - while (x >= pm_range) x -= (2.0f * pm_range); - while (x < -pm_range) x += (2.0f * pm_range); - return x; -} - -//beware of inserting large angles! -static inline float wrap_pm_pi(float theta) { - return wrap_pm(theta, M_PI); -} - // like fmodf, but always positive static inline float fmodf_pos(float x, float y) { float out = fmodf(x, y); @@ -89,6 +92,19 @@ static inline float fmodf_pos(float x, float y) { return out; } +/** + * @brief Similar to modulo operator, except that the output range is centered + * around zero. + * The returned value is always in the range [-pm_range, pm_range). + */ +static inline float wrap_pm(float x, float pm_range) { + return fmodf_pos(x + pm_range, 2.0f * pm_range) - pm_range; +} + +static inline float wrap_pm_pi(float theta) { + return wrap_pm(theta, M_PI); +} + // Compute rising edge timings (0.0 - 1.0) as a function of alpha-beta // as per the magnitude invariant clarke transform // The magnitude of the alpha-beta vector may not be larger than sqrt(3)/2 diff --git a/Firmware/Tests/test_can.cpp b/Firmware/Tests/test_can.cpp new file mode 100644 index 00000000..5a0db2fe --- /dev/null +++ b/Firmware/Tests/test_can.cpp @@ -0,0 +1,90 @@ + +#include +#include +#include + +#include "communication/can_helpers.hpp" + +enum InputMode { + INPUT_MODE_INACTIVE, + INPUT_MODE_PASSTHROUGH, + INPUT_MODE_VEL_RAMP, + INPUT_MODE_POS_FILTER, + INPUT_MODE_MIX_CHANNELS, + INPUT_MODE_TRAP_TRAJ, +}; + +TEST_SUITE("CAN Functions") { + TEST_CASE("reverse") { + can_Message_t rxmsg; + rxmsg.id = 0x000; + rxmsg.isExt = false; + rxmsg.len = 8; + + rxmsg.buf[0] = 0x12; + rxmsg.buf[1] = 0x34; + + std::reverse(std::begin(rxmsg.buf), std::end(rxmsg.buf)); + CHECK(rxmsg.buf[0] == 0x00); + CHECK(rxmsg.buf[6] == 0x34); + CHECK(rxmsg.buf[7] == 0x12); + } + + TEST_CASE("getSignal") { + can_Message_t rxmsg; + + auto val = 0x1234; + std::memcpy(rxmsg.buf, &val, sizeof(val)); + + val = can_getSignal(rxmsg, 0, 16, true, 1, 0); + CHECK(val == 0x1234); + + val = can_getSignal(rxmsg, 0, 16, false, 1, 0); + CHECK(val == 0x3412); + + float myFloat = 1234.6789f; + std::memcpy(rxmsg.buf, &myFloat, sizeof(myFloat)); + auto floatVal = can_getSignal(rxmsg, 0, 32, true, 1, 0); + CHECK(floatVal == 1234.6789f); + + can_Message_t msg; + msg.id = 0x00E; + msg.buf[0] = 0x96; + msg.buf[1] = 0x00; + msg.buf[2] = 0x00; + msg.buf[3] = 0x00; + CHECK(can_getSignal(msg, 0, 32, true, 0.01f, 0.0f) == 1.50f); + } + + TEST_CASE("setSignal") { + can_Message_t txmsg; + + can_setSignal(txmsg, 0x1234, 0, 16, true, 1.0f, 0.0f); + CHECK(can_getSignal(txmsg, 0, 16, true, 1.0f, 0.0f) == 0x1234); + + can_setSignal(txmsg, 0xABCD, 16, 16, true, 1.0f, 0.0f); + CHECK(can_getSignal(txmsg, 0, 16, true, 1.0f, 0.0f) == 0x1234); + CHECK(can_getSignal(txmsg, 16, 16, true, 1.0f, 0.0f) == 0xABCD); + + can_setSignal(txmsg, 1234.5678f, 32, 32, true, 1.0f, 0.0f); + CHECK(can_getSignal(txmsg, 0, 16, true, 1.0f, 0.0f) == 0x1234); + CHECK(can_getSignal(txmsg, 16, 16, true, 1.0f, 0.0f) == 0xABCD); + CHECK(can_getSignal(txmsg, 32, 32, true, 1.0f, 0.0f)); + + can_setSignal(txmsg, 0x1234, 0, 16, false, 1.0f, 0.0f); + CHECK(can_getSignal(txmsg, 0, 16, false, 1.0f, 0.0f) == 0x1234); + CHECK(can_getSignal(txmsg, 16, 16, true, 1.0f, 0.0f) == 0xABCD); + CHECK(can_getSignal(txmsg, 32, 32, true, 1.0f, 0.0f)); + + can_setSignal(txmsg, 234981.0f, 12, 32, false, 2.0f, 1.1f); + CHECK(can_getSignal(txmsg, 12, 32, false, 2.0f, 1.1f) == 234981.0f); + } + + TEST_CASE("getSignal enums") { + can_Message_t rxmsg; + rxmsg.buf[0] = INPUT_MODE_MIX_CHANNELS; + rxmsg.buf[1] = INPUT_MODE_PASSTHROUGH; + CHECK(static_cast(can_getSignal(rxmsg, 0, 8, true, 1, 0)) == INPUT_MODE_MIX_CHANNELS); + CHECK(static_cast(can_getSignal(rxmsg, 8, 8, true, 1, 0)) == INPUT_MODE_PASSTHROUGH); + } +} \ No newline at end of file diff --git a/Firmware/Tests/test_rotate.cpp b/Firmware/Tests/test_rotate.cpp new file mode 100644 index 00000000..b77376cc --- /dev/null +++ b/Firmware/Tests/test_rotate.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include +#include + +TEST_CASE("Rotate Axis State"){ + std::array testArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + const int& currentVal = testArr.front(); + CHECK(currentVal == testArr[0]); + + std::rotate(testArr.begin(), testArr.begin() + 1, testArr.end()); + CHECK(currentVal == testArr[0]); + CHECK(currentVal == 1); + + CHECK(testArr.back() == 0); + + std::rotate(testArr.begin(), testArr.begin() + 1, testArr.end()); + CHECK(currentVal == testArr[0]); + CHECK(currentVal == 2); + CHECK(testArr.back() == 1); + + +} + +TEST_CASE("Fill Test"){ + std::array testArr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + std::fill(testArr.begin(), testArr.end(), 6); + for(const auto& val : testArr){ + CHECK(val == 6); + } +} \ No newline at end of file diff --git a/Firmware/Tests/test_runner.cpp b/Firmware/Tests/test_runner.cpp new file mode 100644 index 00000000..e111405f --- /dev/null +++ b/Firmware/Tests/test_runner.cpp @@ -0,0 +1,157 @@ + + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#define DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_WINDOWS_SEH +#define DOCTEST_CONFIG_NO_POSIX_SIGNALS +// #define DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +#include + +#include + +using std::cout; +using std::endl; + +TEST_SUITE("delta_enc") { + // Modulo (as opposed to remainder), per https://stackoverflow.com/a/19288271 + int mod(int dividend, int divisor) { + int r = dividend % divisor; + return (r < 0) ? (r + divisor) : r; + } + + int getDelta(int pos_abs, int count_in_cpr, int cpr) { + int delta_enc = pos_abs - count_in_cpr; + delta_enc = mod(delta_enc, cpr); + if (delta_enc > (cpr / 2)) + delta_enc -= cpr; + return delta_enc; + } + + TEST_CASE("mod") { + int cpr = 1000; + + // Check moves around 0 + CHECK(getDelta(1, 0, cpr) == 1); + CHECK(getDelta(0, 1, cpr) == -1); + CHECK(getDelta(999, 0, cpr) == -1); + CHECK(getDelta(50, 650, cpr) == 400); + CHECK(getDelta(650, 50, cpr) == -400); + CHECK(getDelta(50, 500, cpr) == -450); + CHECK(getDelta(500, 50, cpr) == 450); + + // Test moving a distance larger than cpr / 2 + CHECK(getDelta(950, 450, cpr) == 500); + CHECK(getDelta(451, 950, cpr) == -499); + CHECK(getDelta(450, 950, cpr) == 500); + + // Test handling around mid-point + CHECK(getDelta(501, 499, cpr) == 2); + CHECK(getDelta(499, 501, cpr) == -2); + CHECK(getDelta(550, 450, cpr) == 100); + CHECK(getDelta(450, 550, cpr) == -100); + } +} + +TEST_SUITE("velLimiter") { +// Velocity limiting in current mode +#include + using doctest::Approx; + + auto limitVel(float vel_limit, float vel_estimate, float vel_gain, float Iq) { + float Imax = (vel_limit - vel_estimate) * vel_gain; + float Imin = (-vel_limit - vel_estimate) * vel_gain; + return std::clamp(Iq, Imin, Imax); + } + + TEST_CASE("limit Vel") { + CHECK(limitVel(0, 0, 0, 0) == 0.0f); + CHECK(limitVel(1000.0f, 1.0f, 0.0f, 0.0f) == 0.0f); + CHECK(limitVel(1000.0f, 500.0f, 1.0f, 1.0f) == 1.0f); + CHECK(limitVel(1000.0f, 500.0f, 1.0f, -20.0f) == -20.0f); + CHECK(limitVel(1000.0f, 999.0f, 1.0f, 2.0f) == 1.0f); + CHECK(limitVel(1000.0f, 999.0f, 1.0f, -5.0f) == -5.0f); + CHECK(limitVel(1000.0f, -999.0f, 1.0f, -5.0f) == -1.0f); + CHECK(limitVel(1000.0f, -999.0f, 1.0f, 5.0f) == 5.0f); + CHECK(limitVel(1000.0f, 0.0f, 1.0f, 1.0f) == 1.0f); + CHECK(limitVel(1000.0f, 0.0f, 1.0f, -1.0f) == -1.0f); + } + + TEST_CASE("Accelerating") { + CHECK(limitVel(200000.0f, 195000.0f, 5.0E-4f, 30.0f) == 2.5f); + CHECK(limitVel(200000.0f, 205000.0f, 5.0E-4f, 30.0f) == -2.5f); + CHECK(limitVel(200000.0f, -195000.0f, 5.0E-4, -30.0f) == -2.5f); + CHECK(limitVel(200000.0f, -205000.0f, 5.0E-4f, -30.0f) == 2.5f); + } + + TEST_CASE("Decelerating") { + CHECK(limitVel(200000.0f, 195000.0f, 5.0E-4f, -30.0f) == -30.0f); + CHECK(limitVel(200000.0f, 205000.0f, 5.0E-4f, -30.0f) == -30.0f); + CHECK(limitVel(200000.0f, -195000.0f, 5.0E-4, 30.0f) == 30.0f); + CHECK(limitVel(200000.0f, -205000.0f, 5.0E-4f, 30.0f) == 30.0f); + } + + TEST_CASE("Over-Center") { + CHECK(limitVel(20000.0f, 1000.0f, 5.0E-4f, 30.0f) == 9.5f); + CHECK(limitVel(20000.0f, -1000.0f, 5.0E-4f, 30.0f) == Approx(10.5f)); + } +} + +TEST_SUITE("vel_ramp") { + float vel_ramp_old(float input_vel_, float vel_setpoint_, float vel_ramp_rate) { + float max_step_size = 0.000125f * vel_ramp_rate; + float full_step = input_vel_ - vel_setpoint_; + float step; + if (std::abs(full_step) > max_step_size) { + step = std::copysignf(max_step_size, full_step); + } else { + step = full_step; + } + return step; + } + + float vel_ramp_new(float input_vel_, float vel_setpoint_, float vel_ramp_rate) { + float max_step_size = 0.000125f * vel_ramp_rate; + float full_step = input_vel_ - vel_setpoint_; + return std::clamp(full_step, -max_step_size, max_step_size); + } + + uint8_t parity(uint16_t v) { + v ^= v >> 8; + v ^= v >> 4; + v ^= v >> 2; + v ^= v >> 1; + return v & 1; + } + + TEST_CASE("Equivalence") { + float vel_setpoint = 0.0f; + float vel_ramp_rate = 8000; + float input_vel = 0.0f; + CHECK(vel_ramp_old(input_vel, vel_setpoint, vel_ramp_rate) == vel_ramp_new(input_vel, vel_setpoint, vel_ramp_rate)); + + input_vel = 10.0f; + CHECK(vel_ramp_old(input_vel, vel_setpoint, vel_ramp_rate) == vel_ramp_new(input_vel, vel_setpoint, vel_ramp_rate)); + + input_vel = 10000.0f; + CHECK(vel_ramp_old(input_vel, vel_setpoint, vel_ramp_rate) == vel_ramp_new(input_vel, vel_setpoint, vel_ramp_rate)); + + input_vel = -10000.0f; + CHECK(vel_ramp_old(input_vel, vel_setpoint, vel_ramp_rate) == vel_ramp_new(input_vel, vel_setpoint, vel_ramp_rate)); + + input_vel = -0.1234f; + CHECK(vel_ramp_old(input_vel, vel_setpoint, vel_ramp_rate) == vel_ramp_new(input_vel, vel_setpoint, vel_ramp_rate)); + + input_vel = 0.1234f; + CHECK(vel_ramp_old(input_vel, vel_setpoint, vel_ramp_rate) == vel_ramp_new(input_vel, vel_setpoint, vel_ramp_rate)); + } + + TEST_CASE("Parity") { + CHECK(parity(0x0DDF & 0x7FFF) == 0); + CHECK(parity(0x8DDF & 0x7FFF) == 0); + CHECK(parity(0x5BFF & 0x7FFF) == 1); + } +} \ No newline at end of file diff --git a/Firmware/Tests/test_timer.cpp b/Firmware/Tests/test_timer.cpp new file mode 100644 index 00000000..89487f93 --- /dev/null +++ b/Firmware/Tests/test_timer.cpp @@ -0,0 +1,29 @@ +#include +#include "MotorControl/timer.hpp" +#include + +TEST_CASE_TEMPLATE("Timer2", T, float, int, char, uint32_t){ + Timer myTimer; + myTimer.setTimeout(10); + myTimer.setIncrement(1); + CHECK(!myTimer.expired()); + + myTimer.start(); + CHECK(!myTimer.expired()); + for(int i = 0; i < 9; ++i){ + myTimer.update(); + CHECK(!myTimer.expired()); + } + + myTimer.update(); + CHECK(myTimer.expired()); + + myTimer.stop(); + CHECK(myTimer.expired()); + + myTimer.start(); + CHECK(myTimer.expired()); + + myTimer.reset(); + CHECK(!myTimer.expired()); +} \ No newline at end of file diff --git a/Firmware/Tests/test_trap_traj.cpp b/Firmware/Tests/test_trap_traj.cpp new file mode 100644 index 00000000..b5d30014 --- /dev/null +++ b/Firmware/Tests/test_trap_traj.cpp @@ -0,0 +1,235 @@ + +#include +#include +#include +#include +#include + +#include "MotorControl/utils.hpp" + +// TODO: This is currently a copy-paste of the real code due to non-trivial +// include dependencies. Should include real code. + +class TrapezoidalTrajectory { +public: + struct Step_t { + float Y; + float Yd; + float Ydd; + }; + + explicit TrapezoidalTrajectory(); + bool planTrapezoidal(float Xf, float Xi, float Vi, + float Vmax, float Amax, float Dmax); + Step_t eval(float t); + + float Xi_; + float Xf_; + float Vi_; + + float Ar_; + float Vr_; + float Dr_; + + float Ta_; + float Tv_; + float Td_; + float Tf_; + + float yAccel_; + + float t_; +}; + + + +// A sign function where input 0 has positive sign (not 0) +float sign_hard(float val) { + return (std::signbit(val)) ? -1.0f : 1.0f; +} + +// Symbol Description +// Ta, Tv and Td Duration of the stages of the AL profile +// Xi and Vi Adapted initial conditions for the AL profile +// Xf Position set-point +// s Direction (sign) of the trajectory +// Vmax, Amax, Dmax and jmax Kinematic bounds +// Ar, Dr and Vr Reached values of acceleration and velocity + +TrapezoidalTrajectory::TrapezoidalTrajectory() {} + +bool TrapezoidalTrajectory::planTrapezoidal(float Xf, float Xi, float Vi, + float Vmax, float Amax, float Dmax) { + float dX = Xf - Xi; // Distance to travel + float stop_dist = (Vi * Vi) / (2.0f * Dmax); // Minimum stopping distance + float dXstop = std::copysign(stop_dist, Vi); // Minimum stopping displacement + float s = sign_hard(dX - dXstop); // Sign of coast velocity (if any) + Ar_ = s * Amax; // Maximum Acceleration (signed) + Dr_ = -s * Dmax; // Maximum Deceleration (signed) + Vr_ = s * Vmax; // Maximum Velocity (signed) + + // If we start with a speed faster than cruising, then we need to decel instead of accel + // aka "double deceleration move" in the paper + if ((s * Vi) > (s * Vr_)) { + Ar_ = -s * Amax; + } + + // Time to accel/decel to/from Vr (cruise speed) + Ta_ = (Vr_ - Vi) / Ar_; + Td_ = -Vr_ / Dr_; + + // Integral of velocity ramps over the full accel and decel times to get + // minimum displacement required to reach cuising speed + float dXmin = 0.5f*Ta_*(Vr_ + Vi) + 0.5f*Td_*Vr_; + + // Are we displacing enough to reach cruising speed? + if (s*dX < s*dXmin) { + // Short move (triangle profile) + Vr_ = s * sqrtf(std::fmax((Dr_*SQ(Vi) + 2*Ar_*Dr_*dX) / (Dr_ - Ar_), 0.0f)); + //Vr_ = s * sqrtf((Dr_*SQ(Vi) + 2*Ar_*Dr_*dX) / (Dr_ - Ar_)); + Ta_ = std::max(0.0f, (Vr_ - Vi) / Ar_); + Td_ = std::max(0.0f, -Vr_ / Dr_); + Tv_ = 0.0f; + } else { + // Long move (trapezoidal profile) + Tv_ = (dX - dXmin) / Vr_; + } + + // Fill in the rest of the values used at evaluation-time + Tf_ = Ta_ + Tv_ + Td_; + Xi_ = Xi; + Xf_ = Xf; + Vi_ = Vi; + yAccel_ = Xi + Vi*Ta_ + 0.5f*Ar_*SQ(Ta_); // pos at end of accel phase + + return true; +} + +TrapezoidalTrajectory::Step_t TrapezoidalTrajectory::eval(float t) { + Step_t trajStep; + if (t < 0.0f) { // Initial Condition + trajStep.Y = Xi_; + trajStep.Yd = Vi_; + trajStep.Ydd = 0.0f; + } else if (t < Ta_) { // Accelerating + trajStep.Y = Xi_ + Vi_*t + 0.5f*Ar_*SQ(t); + trajStep.Yd = Vi_ + Ar_*t; + trajStep.Ydd = Ar_; + } else if (t < Ta_ + Tv_) { // Coasting + trajStep.Y = yAccel_ + Vr_*(t - Ta_); + trajStep.Yd = Vr_; + trajStep.Ydd = 0.0f; + } else if (t < Tf_) { // Deceleration + float td = t - Tf_; + trajStep.Y = Xf_ + 0.5f*Dr_*SQ(td); + trajStep.Yd = Dr_*td; + trajStep.Ydd = Dr_; + } else if (t >= Tf_) { // Final Condition + trajStep.Y = Xf_; + trajStep.Yd = 0.0f; + trajStep.Ydd = 0.0f; + } else { + // TODO: report error here + } + + return trajStep; +} + +static_assert(sizeof(float) * CHAR_BIT == 32); + + +void run_trajectory_test(float goal, float position, float velocity, float Vmax, float Amax, float Dmax) { + float dt = 0.000125f; + int replan_interval = 10; // must be > 2 (see note below) + float t = 0.0f; + float Vmax_test = std::max(Vmax, std::abs(velocity)); + + TrapezoidalTrajectory traj{}; + + int replan_counter = 0; + + do { + if (replan_counter <= 0) { + CHECK(traj.planTrapezoidal(goal, position, velocity, Vmax, Amax, Dmax)); + t = 0.0f; + replan_counter = replan_interval; + } else { + replan_counter--; + } + + TrapezoidalTrajectory::Step_t step = traj.eval(t); + t += dt; + + //std::cerr << "vel: " << step.Yd << ", pos: " << step.Y << "\n"; + + // Check if acceleration within bounds + if (velocity >= 0.0f) { + CHECK(step.Ydd <= Amax); + CHECK(step.Ydd >= -Dmax); + CHECK((step.Yd - velocity) / dt <= Amax * 1.002f); + CHECK((step.Yd - velocity) / dt >= -Dmax * 1.002f); + } else { + CHECK(step.Ydd <= Dmax); + CHECK(step.Ydd >= -Amax); + CHECK((step.Yd - velocity) / dt <= Dmax * 1.002f); + CHECK((step.Yd - velocity) / dt >= -Amax * 1.002f); + } + + // Check if velocity within bounds + CHECK(step.Yd >= -Vmax_test); + CHECK(step.Yd <= Vmax_test); + CHECK((step.Y - position) / dt >= -Vmax_test * 1.002f); + CHECK((step.Y - position) / dt <= Vmax_test * 1.002f); + velocity = step.Yd; + + // Check if position is making progress + // TODO: the trajectory planner currently needs three "warm-up" iterations + // until its position makes progress. This should probably be revisited. + // TODO: this is disabled currently because there are legitimate trajectories + // where the position first moves in the wrong direction. + //if ((replan_counter < replan_interval - 2) && (t <= traj.Tf_)) { + // CHECK(std::abs(step.Y - goal) < std::abs(position - goal)); + //} + position = step.Y; + + } while (t <= traj.Tf_); + + CHECK(position >= goal - 1.0f); + CHECK(position <= goal + 1.0f); + CHECK(velocity >= -Dmax * dt); + CHECK(velocity <= Dmax * dt); +} + + +TEST_SUITE("Trajectory Planner") { + // these form a triangle trajectory because 2*v^2/(2*a) = 2 * 27712^2 / (2*22288) = 34456 > 16384 + TEST_CASE("neg-dir-triangle") { + run_trajectory_test(-8192.0f, 8192.0f, 0.0f, 27712.0f, 22288.0f, 22288.0f); + } + TEST_CASE("pos-dir-triangle") { + run_trajectory_test(8192.0f, -8192.0f, 0.0f, 27712.0f, 22288.0f, 22288.0f); + } + + // these form a trapezoid trajectory because 2*v^2/(2*a) = 2 * 27712^2 / (2*22288) = 34456 < 16384 + TEST_CASE("neg-dir-trapezoid") { + run_trajectory_test(-25000.0f, 25000.0f, 0.0f, 27712.0f, 22288.0f, 22288.0f); + } + TEST_CASE("pos-dir-trapezoid") { + run_trajectory_test(25000.0f, -25000.0f, 0.0f, 27712.0f, 22288.0f, 22288.0f); + } + + // for the following tests note that v^2/(2*a) = 27712^2 / (2*22288) = 17227 > 16384 + TEST_CASE("neg-dir-not-enough-braking-distance") { + run_trajectory_test(-8192.0f, 8192.0f, -27712.0f, 27712.0f, 22288.0f, 22288.0f); + } + TEST_CASE("pos-dir-not-enough-braking-distance") { + run_trajectory_test(8192.0f, -8192.0f, 27712.0f, 27712.0f, 22288.0f, 22288.0f); + } + + TEST_CASE("neg-dir-over-speed") { + run_trajectory_test(-8192.0f, 8192.0f, -40000.0f, 27712.0f, 22288.0f, 22288.0f); + } + TEST_CASE("pos-dir-over-speed") { + run_trajectory_test(8192.0f, -8192.0f, 40000.0f, 27712.0f, 22288.0f, 22288.0f); + } +} diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index 994fd746..7f5380ae 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -1,6 +1,38 @@ tup.include('build.lua') +-- If we simply invoke python or python3 on a pristine Windows 10, it will try +-- to open the Microsoft Store which will not work and hang tup instead. The +-- command "python --version" does not open the Microsoft Store. +-- On some systems this may return a python2 command if Python3 is not installed. +function find_python3() + success, python_version = run_now("python --version 2>&1") + if success and string.match(python_version, "Python 3") then return "python -B" end + success, python_version = run_now("python3 --version 2>&1") + if success and string.match(python_version, "Python 3") then return "python3 -B" end + error("Python 3 not found.") +end + +python_command = find_python3() +print('Using python command "'..python_command..'"') + +tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'} +tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'} +tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --generate-endpoints ODrive --template %f --output %o', outputs='autogen/endpoints.hpp'} +tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'} + +-- Note: we currently check this file into source control for two reasons: +-- - Don't require tup to run in order to use odrivetool from the repo +-- - On Windows, tup is unhappy with writing outside of the tup directory +-- TODO: use CI to verify that on PRs the enums.py file is consistent with the YAML. +--tup.frule{command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'} + +tup.frule{ + command=python_command..' ../tools/odrive/version.py --output %o', + outputs={'autogen/version.c'} +} + + -- Switch between board versions boardversion = tup.getconfig("BOARD_VERSION") if boardversion == "v3.1" then @@ -86,7 +118,6 @@ if tup.getconfig("STRICT") == "true" then FLAGS += '-Werror' end - -- C-specific flags FLAGS += '-D__weak="__attribute__((weak))"' FLAGS += '-D__packed="__attribute__((__packed__))"' @@ -145,10 +176,6 @@ build{ includes=stm_includes } -tup.frule{ - command='python ../tools/odrive/version.py --output %o', - outputs={'build/version.h'} -} build{ name='ODriveFirmware', @@ -157,18 +184,21 @@ build{ packages={'stm_platform'}, sources={ 'Drivers/DRV8301/drv8301.c', - 'MotorControl/utils.c', + 'MotorControl/utils.cpp', 'MotorControl/arm_sin_f32.c', 'MotorControl/arm_cos_f32.c', 'MotorControl/low_level.cpp', 'MotorControl/nvm.c', 'MotorControl/axis.cpp', 'MotorControl/motor.cpp', + 'MotorControl/thermistor.cpp', 'MotorControl/encoder.cpp', + 'MotorControl/endstop.cpp', 'MotorControl/controller.cpp', 'MotorControl/sensorless_estimator.cpp', 'MotorControl/trapTraj.cpp', 'MotorControl/main.cpp', + 'communication/can_simple.cpp', 'communication/communication.cpp', 'communication/ascii_protocol.cpp', 'communication/interface_uart.cpp', @@ -176,12 +206,21 @@ build{ 'communication/interface_can.cpp', 'communication/interface_i2c.cpp', 'fibre/cpp/protocol.cpp', - 'FreeRTOS-openocd.c' + 'FreeRTOS-openocd.c', + 'autogen/version.c' }, includes={ 'Drivers/DRV8301', 'MotorControl', 'fibre/cpp/include', - '.' + '.', + "doctest" } } + +if tup.getconfig('DOCTEST') == 'true' then + TEST_INCLUDES = '-I. -I./MotorControl -I./fibre/cpp/include -I./Drivers/DRV8301 -I./doctest' + tup.foreach_rule('Tests/*.cpp', 'g++ -O3 -std=c++17 '..TEST_INCLUDES..' -c %f -o %o', 'Tests/bin/%B.o') + tup.frule{inputs='Tests/bin/*.o', command='g++ %f -o %o', outputs='Tests/test_runner.exe'} + tup.frule{inputs='Tests/test_runner.exe', command='%f'} +end diff --git a/Firmware/build.lua b/Firmware/build.lua index d4c7aad4..e6d93e41 100644 --- a/Firmware/build.lua +++ b/Firmware/build.lua @@ -12,6 +12,14 @@ function string:split(sep) return fields end +function run_now(command) + local handle + handle = io.popen(command) + local output = handle:read("*a") + local rc = {handle:close()} + return rc[1], output +end + -- Very basic parser to retrieve variables from a Makefile function parse_makefile_vars(makefile) vars = {} @@ -72,7 +80,7 @@ function GCCToolchain(prefix, builddir, compiler_flags, linker_flags) else extra_outputs = {} end - if src == 'communication/communication.cpp' then extra_inputs = 'build/version.h' end -- TODO: fix hack + extra_inputs = {'autogen/interfaces.hpp', 'autogen/function_stubs.hpp', 'autogen/endpoints.hpp', 'autogen/type_info.hpp'} -- TODO: fix hack tup.frule{ inputs= { src, extra_inputs=extra_inputs }, command=compiler..' -c %f '.. @@ -85,7 +93,7 @@ function GCCToolchain(prefix, builddir, compiler_flags, linker_flags) end return { compile_c = function(src, flags, includes, outputs) gcc_generic_compiler(prefix..'gcc -std=c99', compiler_flags, calculate_stack_usage, src, flags, includes, outputs) end, - compile_cpp = function(src, flags, includes, outputs) gcc_generic_compiler(prefix..'g++ -std=c++14', compiler_flags, calculate_stack_usage, src, flags, includes, outputs) end, + compile_cpp = function(src, flags, includes, outputs) gcc_generic_compiler(prefix..'g++ -std=c++17 -Wno-register', compiler_flags, calculate_stack_usage, src, flags, includes, outputs) end, compile_asm = function(src, flags, includes, outputs) gcc_generic_compiler(prefix..'gcc -x assembler-with-cpp', compiler_flags, false, src, flags, includes, outputs) end, link = function(objects, output_name) output_name = builddir..'/'..output_name diff --git a/Firmware/build.sh b/Firmware/build.sh deleted file mode 100755 index 8f3a0730..00000000 --- a/Firmware/build.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Builds the firmware with the configuration specified by -# environment variables named CONFIG_... -# If DEPLOY is set, the deliverables are copied to Firmware/deploy/* -# with the suffix $DEPLOY -set -euo pipefail - -THIS_DIR="$(dirname "$0")" -cd "$THIS_DIR" - -# Treat warnings as errors -export CONFIG_STRICT=true - -# Write all environment variables that start with "CONFIG_" to tup.config -rm -rdf build -mkdir -p build -env | grep ^CONFIG > tup.config -tup init -tup generate ./tup_build.sh -bash -xe ./tup_build.sh - -# Deploy -if ! [ -z ${DEPLOY+x} ]; then - mkdir -p deploy - cp build/ODriveFirmware.elf deploy/ODriveFirmware_"$DEPLOY".elf - cp build/ODriveFirmware.hex deploy/ODriveFirmware_"$DEPLOY".hex -fi diff --git a/Firmware/communication/ascii_protocol.cpp b/Firmware/communication/ascii_protocol.cpp index 7e413ebe..bce84d7a 100644 --- a/Firmware/communication/ascii_protocol.cpp +++ b/Firmware/communication/ascii_protocol.cpp @@ -8,12 +8,14 @@ /* Includes ------------------------------------------------------------------*/ #include "odrive_main.h" -#include "../build/version.h" // autogenerated based on Git state #include "communication.h" #include "ascii_protocol.hpp" #include #include +#include "autogen/type_info.hpp" +#include "communication/interface_can.hpp" + /* Private macros ------------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/ /* Global constant data ------------------------------------------------------*/ @@ -25,12 +27,15 @@ #define TO_STR(s) TO_STR_INNER(s) /* Private variables ---------------------------------------------------------*/ + +static Introspectable root_obj = ODriveTypeInfo::make_introspectable(odrv); + /* Private function prototypes -----------------------------------------------*/ void cmd_set_position(char * pStr, StreamSink& response_channel, bool use_checksum); void cmd_set_position_wl(char * pStr, StreamSink& response_channel, bool use_checksum); void cmd_set_velocity(char * pStr, StreamSink& response_channel, bool use_checksum); -void cmd_set_current(char * pStr, StreamSink& response_channel, bool use_checksum); +void cmd_set_torque(char * pStr, StreamSink& response_channel, bool use_checksum); void cmd_set_trapezoid_trajectory(char * pStr, StreamSink& response_channel, bool use_checksum); void cmd_get_feedback(char * pStr, StreamSink& response_channel, bool use_checksum); void cmd_help(char * pStr, StreamSink& response_channel, bool use_checksum); @@ -46,15 +51,16 @@ void cmd_unknown(char * pStr, StreamSink& response_channel, bool use_checksum); // @brief Sends a line on the specified output. template void respond(StreamSink& output, bool include_checksum, const char * fmt, TArgs&& ... args) { - char response[64]; - + char response[64]; // Hardcoded max buffer size. We silently truncate the output if it's too long for the buffer. size_t len = snprintf(response, sizeof(response), fmt, std::forward(args)...); + len = std::min(len, sizeof(response)); output.process_bytes((uint8_t*)response, len, nullptr); // TODO: use process_all instead if (include_checksum) { uint8_t checksum = 0; for (size_t i = 0; i < len; ++i) checksum ^= response[i]; len = snprintf(response, sizeof(response), "*%u", checksum); + len = std::min(len, sizeof(response)); output.process_bytes((uint8_t*)response, len, nullptr); } output.process_bytes((const uint8_t*)"\r\n", 2, nullptr); @@ -107,7 +113,7 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& case 'p': cmd_set_position(cmd, response_channel, use_checksum); break; // position control case 'q': cmd_set_position_wl(cmd, response_channel, use_checksum); break; // position control with limits case 'v': cmd_set_velocity(cmd, response_channel, use_checksum); break; // velocity control - case 'c': cmd_set_current(cmd, response_channel, use_checksum); break; // current control + case 'c': cmd_set_torque(cmd, response_channel, use_checksum); break; // current control case 't': cmd_set_trapezoid_trajectory(cmd, response_channel, use_checksum); break; // trapezoidal trajectory case 'f': cmd_get_feedback(cmd, response_channel, use_checksum); break; // feedback case 'h': cmd_help(cmd, response_channel, use_checksum); break; // Help @@ -126,21 +132,21 @@ void ASCII_protocol_process_line(const uint8_t* buffer, size_t len, StreamSink& // @param use_checksum bool to indicate whether a checksum is required on response void cmd_set_position(char * pStr, StreamSink& response_channel, bool use_checksum) { unsigned motor_number; - float pos_setpoint, vel_feed_forward, current_feed_forward; + float pos_setpoint, vel_feed_forward, torque_feed_forward; - int numscan = sscanf(pStr, "p %u %f %f %f", &motor_number, &pos_setpoint, &vel_feed_forward, ¤t_feed_forward); + int numscan = sscanf(pStr, "p %u %f %f %f", &motor_number, &pos_setpoint, &vel_feed_forward, &torque_feed_forward); if (numscan < 2) { respond(response_channel, use_checksum, "invalid command format"); } else if (motor_number >= AXIS_COUNT) { respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_POSITION_CONTROL; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL; axis->controller_.input_pos_ = pos_setpoint; if (numscan >= 3) axis->controller_.input_vel_ = vel_feed_forward; if (numscan >= 4) - axis->controller_.input_current_ = current_feed_forward; + axis->controller_.input_torque_ = torque_feed_forward; axis->controller_.input_pos_updated(); axis->watchdog_feed(); } @@ -152,21 +158,21 @@ void cmd_set_position(char * pStr, StreamSink& response_channel, bool use_checks // @param use_checksum bool to indicate whether a checksum is required on response void cmd_set_position_wl(char * pStr, StreamSink& response_channel, bool use_checksum) { unsigned motor_number; - float pos_setpoint, vel_limit, current_lim; + float pos_setpoint, vel_limit, torque_lim; - int numscan = sscanf(pStr, "q %u %f %f %f", &motor_number, &pos_setpoint, &vel_limit, ¤t_lim); + int numscan = sscanf(pStr, "q %u %f %f %f", &motor_number, &pos_setpoint, &vel_limit, &torque_lim); if (numscan < 2) { respond(response_channel, use_checksum, "invalid command format"); } else if (motor_number >= AXIS_COUNT) { respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_POSITION_CONTROL; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL; axis->controller_.input_pos_ = pos_setpoint; if (numscan >= 3) axis->controller_.config_.vel_limit = vel_limit; if (numscan >= 4) - axis->motor_.config_.current_lim = current_lim; + axis->motor_.config_.torque_lim = torque_lim; axis->controller_.input_pos_updated(); axis->watchdog_feed(); } @@ -178,38 +184,38 @@ void cmd_set_position_wl(char * pStr, StreamSink& response_channel, bool use_che // @param use_checksum bool to indicate whether a checksum is required on response void cmd_set_velocity(char * pStr, StreamSink& response_channel, bool use_checksum) { unsigned motor_number; - float vel_setpoint, current_feed_forward; - int numscan = sscanf(pStr, "v %u %f %f", &motor_number, &vel_setpoint, ¤t_feed_forward); + float vel_setpoint, torque_feed_forward; + int numscan = sscanf(pStr, "v %u %f %f", &motor_number, &vel_setpoint, &torque_feed_forward); if (numscan < 2) { respond(response_channel, use_checksum, "invalid command format"); } else if (motor_number >= AXIS_COUNT) { respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_VELOCITY_CONTROL; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_VELOCITY_CONTROL; axis->controller_.input_vel_ = vel_setpoint; if (numscan >= 3) - axis->controller_.input_current_ = current_feed_forward; + axis->controller_.input_torque_ = torque_feed_forward; axis->watchdog_feed(); } } -// @brief Executes the set current limit command +// @brief Executes the set torque control command // @param pStr buffer of ASCII encoded values // @param response_channel reference to the stream to respond on // @param use_checksum bool to indicate whether a checksum is required on response -void cmd_set_current(char * pStr, StreamSink& response_channel, bool use_checksum) { +void cmd_set_torque(char * pStr, StreamSink& response_channel, bool use_checksum) { unsigned motor_number; - float current_setpoint; + float torque_setpoint; - if (sscanf(pStr, "c %u %f", &motor_number, ¤t_setpoint) < 2) { + if (sscanf(pStr, "c %u %f", &motor_number, &torque_setpoint) < 2) { respond(response_channel, use_checksum, "invalid command format"); } else if (motor_number >= AXIS_COUNT) { respond(response_channel, use_checksum, "invalid motor %u", motor_number); } else { Axis* axis = axes[motor_number]; - axis->controller_.config_.control_mode = Controller::CTRL_MODE_CURRENT_CONTROL; - axis->controller_.input_current_ = current_setpoint; + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_TORQUE_CONTROL; + axis->controller_.input_torque_ = torque_setpoint; axis->watchdog_feed(); } } @@ -229,7 +235,9 @@ void cmd_set_trapezoid_trajectory(char * pStr, StreamSink& response_channel, boo } else { Axis* axis = axes[motor_number]; axis->controller_.config_.input_mode = Controller::INPUT_MODE_TRAP_TRAJ; - axis->controller_.move_to_pos(goal_point); + axis->controller_.config_.control_mode = Controller::CONTROL_MODE_POSITION_CONTROL; + axis->controller_.input_pos_ = goal_point; + axis->controller_.input_pos_updated(); axis->watchdog_feed(); } } @@ -265,7 +273,7 @@ void cmd_help(char * pStr, StreamSink& response_channel, bool use_checksum) { respond(response_channel, use_checksum, "Position: q axis pos vel-lim I-lim"); respond(response_channel, use_checksum, "Position: p axis pos vel-ff I-ff"); respond(response_channel, use_checksum, "Velocity: v axis vel I-ff"); - respond(response_channel, use_checksum, "Current: c axis I"); + respond(response_channel, use_checksum, "Torque: c axis T"); respond(response_channel, use_checksum, ""); respond(response_channel, use_checksum, "Properties start at odrive root, such as axis0.requested_state"); respond(response_channel, use_checksum, "Read: r property"); @@ -284,8 +292,8 @@ void cmd_info_dump(char * pStr, StreamSink& response_channel, bool use_checksum) // respond(response_channel, use_checksum, "Signature: %#x", STM_ID_GetSignature()); // respond(response_channel, use_checksum, "Revision: %#x", STM_ID_GetRevision()); // respond(response_channel, use_checksum, "Flash Size: %#x KiB", STM_ID_GetFlashSize()); - respond(response_channel, use_checksum, "Hardware version: %d.%d-%dV", HW_VERSION_MAJOR, HW_VERSION_MINOR, HW_VERSION_VOLTAGE); - respond(response_channel, use_checksum, "Firmware version: %d.%d.%d", FW_VERSION_MAJOR, FW_VERSION_MINOR, FW_VERSION_REVISION); + respond(response_channel, use_checksum, "Hardware version: %d.%d-%dV", odrv.hw_version_major_, odrv.hw_version_minor_, odrv.hw_version_variant_); + respond(response_channel, use_checksum, "Firmware version: %d.%d.%d", odrv.fw_version_major_, odrv.fw_version_minor_, odrv.fw_version_revision_); respond(response_channel, use_checksum, "Serial number: %s", serial_number_str); } @@ -296,10 +304,10 @@ void cmd_info_dump(char * pStr, StreamSink& response_channel, bool use_checksum) void cmd_system_ctrl(char * pStr, StreamSink& response_channel, bool use_checksum) { switch (pStr[1]) { - case 's': save_configuration(); break; // Save config - case 'e': erase_configuration(); break; // Erase config - case 'r': NVIC_SystemReset(); break; // Reboot - default: /* default */ break; + case 's': odrv.save_configuration(); break; // Save config + case 'e': odrv.erase_configuration(); break; // Erase config + case 'r': odrv.reboot(); break; // Reboot + default: /* default */ break; } } @@ -313,12 +321,14 @@ void cmd_read_property(char * pStr, StreamSink& response_channel, bool use_check if (sscanf(pStr, "r %255s", name) < 1) { respond(response_channel, use_checksum, "invalid command format"); } else { - Endpoint* endpoint = application_endpoints_->get_by_name(name, sizeof(name)); - if (!endpoint) { + Introspectable property = root_obj.get_child(name, sizeof(name)); + const StringConvertibleTypeInfo* type_info = dynamic_cast(property.get_type_info()); + if (!type_info) { respond(response_channel, use_checksum, "invalid property"); } else { char response[10]; - respond(response_channel, use_checksum, (endpoint->get_string(response, sizeof(response))) ? response : "not implemented"); + bool success = type_info->get_string(property, response, sizeof(response)); + respond(response_channel, use_checksum, success ? response : "not implemented"); } } } @@ -334,11 +344,13 @@ void cmd_write_property(char * pStr, StreamSink& response_channel, bool use_chec if (sscanf(pStr, "w %255s %255s", name, value) < 1) { respond(response_channel, use_checksum, "invalid command format"); } else { - Endpoint* endpoint = application_endpoints_->get_by_name(name, sizeof(name)); - if (!endpoint) { + Introspectable property = root_obj.get_child(name, sizeof(name)); + const StringConvertibleTypeInfo* type_info = dynamic_cast(property.get_type_info()); + if (!type_info) { respond(response_channel, use_checksum, "invalid property"); } else { - if (!endpoint->set_string(value, sizeof(value))) { + bool success = type_info->set_string(property, value, sizeof(value)); + if (!success) { respond(response_channel, use_checksum, "not implemented"); } } @@ -352,7 +364,7 @@ void cmd_write_property(char * pStr, StreamSink& response_channel, bool use_chec void cmd_update_axis_wdg(char * pStr, StreamSink& response_channel, bool use_checksum) { unsigned motor_number; - if(sscanf(pStr, "u %u", &motor_number) < 1) { + if (sscanf(pStr, "u %u", &motor_number) < 1) { respond(response_channel, use_checksum, "invalid command format"); } else if (motor_number >= AXIS_COUNT) { respond(response_channel, use_checksum, "invalid motor %u", motor_number); diff --git a/Firmware/communication/can_helpers.hpp b/Firmware/communication/can_helpers.hpp new file mode 100644 index 00000000..779b6a7a --- /dev/null +++ b/Firmware/communication/can_helpers.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +struct can_Message_t { + uint32_t id = 0x000; // 11-bit max is 0x7ff, 29-bit max is 0x1FFFFFFF + bool isExt = false; + bool rtr = false; + uint8_t len = 8; + uint8_t buf[8] = {0, 0, 0, 0, 0, 0, 0, 0}; +} ; + +struct can_Signal_t { + const uint8_t startBit; + const uint8_t length; + const bool isIntel; + const float factor; + const float offset; +}; + + +#include +template +T can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel) { + uint64_t tempVal = 0; + uint64_t mask = (1ULL << length) - 1; + + if (isIntel) { + std::memcpy(&tempVal, msg.buf, sizeof(tempVal)); + tempVal = (tempVal >> startBit) & mask; + } else { + std::reverse(std::begin(msg.buf), std::end(msg.buf)); + std::memcpy(&tempVal, msg.buf, sizeof(tempVal)); + tempVal = (tempVal >> (64 - startBit - length)) & mask; + } + + T retVal; + std::memcpy(&retVal, &tempVal, sizeof(T)); + return retVal; +} + +template +float can_getSignal(can_Message_t msg, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) { + T retVal = can_getSignal(msg, startBit, length, isIntel); + return (retVal * factor) + offset; +} + +template +void can_setSignal(can_Message_t& msg, const T& val, const uint8_t startBit, const uint8_t length, const bool isIntel, const float factor, const float offset) { + T scaledVal = (val - offset) / factor; + uint64_t valAsBits = 0; + std::memcpy(&valAsBits, &scaledVal, sizeof(scaledVal)); + + uint64_t mask = (1ULL << length) - 1; + + if (isIntel) { + uint64_t data = 0; + std::memcpy(&data, msg.buf, sizeof(data)); + + data &= ~(mask << startBit); + data |= valAsBits << startBit; + + std::memcpy(msg.buf, &data, sizeof(data)); + } else { + uint64_t data = 0; + std::reverse(std::begin(msg.buf), std::end(msg.buf)); + std::memcpy(&data, msg.buf, sizeof(data)); + + data &= ~(mask << (64 - startBit - length)); + data |= valAsBits << (64 - startBit - length); + + std::memcpy(msg.buf, &data, sizeof(data)); + std::reverse(std::begin(msg.buf), std::end(msg.buf)); + } +} + +template +float can_getSignal(can_Message_t msg, const can_Signal_t& signal) { + return can_getSignal(msg, signal.startBit, signal.length, signal.isIntel, signal.factor, signal.offset); +} + +template +void can_setSignal(can_Message_t& msg, const T& val, const can_Signal_t& signal) { + can_setSignal(msg, val, signal.startBit, signal.length, signal.isIntel, signal.factor, signal.offset); +} \ No newline at end of file diff --git a/Firmware/communication/can_simple.cpp b/Firmware/communication/can_simple.cpp new file mode 100644 index 00000000..f21f4b90 --- /dev/null +++ b/Firmware/communication/can_simple.cpp @@ -0,0 +1,412 @@ + +#include "can_simple.hpp" +#include + +#include + +static constexpr uint8_t NUM_NODE_ID_BITS = 6; +static constexpr uint8_t NUM_CMD_ID_BITS = 11 - NUM_NODE_ID_BITS; + +void CANSimple::handle_can_message(can_Message_t& msg) { + // This functional way of handling the messages is neat and is much cleaner from + // a data security point of view, but it will require some tweaking to fix the syntax. + // + // auto func = callback_map.find(msg.id); + // if(func != callback_map.end()){ + // func->second(msg); + // } + + // Frame + // nodeID | CMD + // 6 bits | 5 bits + uint32_t nodeID = get_node_id(msg.id); + uint32_t cmd = get_cmd_id(msg.id); + + Axis* axis = nullptr; + + bool validAxis = false; + for (uint8_t i = 0; i < AXIS_COUNT; i++) { + if ((axes[i]->config_.can_node_id == nodeID) && (axes[i]->config_.can_node_id_extended == msg.isExt)) { + axis = axes[i]; + if (!validAxis) { + validAxis = true; + } else { + // Duplicate can IDs, don't assign to any axis + odCAN->set_error(ODriveCAN::ERROR_DUPLICATE_CAN_IDS); + validAxis = false; + break; + } + } + } + + if (validAxis) { + axis->watchdog_feed(); + switch (cmd) { + case MSG_CO_NMT_CTRL: + break; + case MSG_CO_HEARTBEAT_CMD: + break; + case MSG_ODRIVE_HEARTBEAT: + // We don't currently do anything to respond to ODrive heartbeat messages + break; + case MSG_ODRIVE_ESTOP: + estop_callback(axis, msg); + break; + case MSG_GET_MOTOR_ERROR: + get_motor_error_callback(axis, msg); + break; + case MSG_GET_ENCODER_ERROR: + get_encoder_error_callback(axis, msg); + break; + case MSG_GET_SENSORLESS_ERROR: + get_sensorless_error_callback(axis, msg); + break; + case MSG_SET_AXIS_NODE_ID: + set_axis_nodeid_callback(axis, msg); + break; + case MSG_SET_AXIS_REQUESTED_STATE: + set_axis_requested_state_callback(axis, msg); + break; + case MSG_SET_AXIS_STARTUP_CONFIG: + set_axis_startup_config_callback(axis, msg); + break; + case MSG_GET_ENCODER_ESTIMATES: + get_encoder_estimates_callback(axis, msg); + break; + case MSG_GET_ENCODER_COUNT: + get_encoder_count_callback(axis, msg); + break; + case MSG_SET_INPUT_POS: + set_input_pos_callback(axis, msg); + break; + case MSG_SET_INPUT_VEL: + set_input_vel_callback(axis, msg); + break; + case MSG_SET_INPUT_TORQUE: + set_input_torque_callback(axis, msg); + break; + case MSG_SET_CONTROLLER_MODES: + set_controller_modes_callback(axis, msg); + break; + case MSG_SET_VEL_LIMIT: + set_vel_limit_callback(axis, msg); + break; + case MSG_START_ANTICOGGING: + start_anticogging_callback(axis, msg); + break; + case MSG_SET_TRAJ_INERTIA: + set_traj_inertia_callback(axis, msg); + break; + case MSG_SET_TRAJ_ACCEL_LIMITS: + set_traj_accel_limits_callback(axis, msg); + break; + case MSG_SET_TRAJ_VEL_LIMIT: + set_traj_vel_limit_callback(axis, msg); + break; + case MSG_GET_IQ: + get_iq_callback(axis, msg); + break; + case MSG_GET_SENSORLESS_ESTIMATES: + get_sensorless_estimates_callback(axis, msg); + break; + case MSG_RESET_ODRIVE: + NVIC_SystemReset(); + break; + case MSG_GET_VBUS_VOLTAGE: + get_vbus_voltage_callback(axis, msg); + break; + case MSG_CLEAR_ERRORS: + clear_errors_callback(axis, msg); + break; + default: + break; + } + } +} + +void CANSimple::nmt_callback(Axis* axis, can_Message_t& msg) { + // Not implemented +} + +void CANSimple::estop_callback(Axis* axis, can_Message_t& msg) { + axis->error_ |= Axis::ERROR_ESTOP_REQUESTED; +} + +void CANSimple::get_motor_error_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_MOTOR_ERROR; // heartbeat ID + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + txmsg.buf[0] = axis->motor_.error_; + txmsg.buf[1] = axis->motor_.error_ >> 8; + txmsg.buf[2] = axis->motor_.error_ >> 16; + txmsg.buf[3] = axis->motor_.error_ >> 24; + + odCAN->write(txmsg); + } +} + +void CANSimple::get_encoder_error_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_ENCODER_ERROR; // heartbeat ID + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + txmsg.buf[0] = axis->encoder_.error_; + txmsg.buf[1] = axis->encoder_.error_ >> 8; + txmsg.buf[2] = axis->encoder_.error_ >> 16; + txmsg.buf[3] = axis->encoder_.error_ >> 24; + + odCAN->write(txmsg); + } +} + +void CANSimple::get_sensorless_error_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_SENSORLESS_ERROR; // heartbeat ID + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + txmsg.buf[0] = axis->sensorless_estimator_.error_; + txmsg.buf[1] = axis->sensorless_estimator_.error_ >> 8; + txmsg.buf[2] = axis->sensorless_estimator_.error_ >> 16; + txmsg.buf[3] = axis->sensorless_estimator_.error_ >> 24; + + odCAN->write(txmsg); + } +} + +void CANSimple::set_axis_nodeid_callback(Axis* axis, can_Message_t& msg) { + axis->config_.can_node_id = can_getSignal(msg, 0, 32, true); +} + +void CANSimple::set_axis_requested_state_callback(Axis* axis, can_Message_t& msg) { + axis->requested_state_ = static_cast(can_getSignal(msg, 0, 16, true)); +} +void CANSimple::set_axis_startup_config_callback(Axis* axis, can_Message_t& msg) { + // Not Implemented +} + +void CANSimple::get_encoder_estimates_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_ENCODER_ESTIMATES; // heartbeat ID + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + // Undefined behaviour! + // uint32_t floatBytes = *(reinterpret_cast(&(axis->encoder_.pos_estimate_))); + + uint32_t floatBytes; + static_assert(sizeof axis->encoder_.pos_estimate_ == sizeof floatBytes); + std::memcpy(&floatBytes, &axis->encoder_.pos_estimate_, sizeof floatBytes); + + txmsg.buf[0] = floatBytes; + txmsg.buf[1] = floatBytes >> 8; + txmsg.buf[2] = floatBytes >> 16; + txmsg.buf[3] = floatBytes >> 24; + + static_assert(sizeof floatBytes == sizeof axis->encoder_.vel_estimate_); + std::memcpy(&floatBytes, &axis->encoder_.vel_estimate_, sizeof floatBytes); + txmsg.buf[4] = floatBytes; + txmsg.buf[5] = floatBytes >> 8; + txmsg.buf[6] = floatBytes >> 16; + txmsg.buf[7] = floatBytes >> 24; + + odCAN->write(txmsg); + } +} + +void CANSimple::get_sensorless_estimates_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_SENSORLESS_ESTIMATES; // heartbeat ID + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + // Undefined behaviour! + // uint32_t floatBytes = *(reinterpret_cast(&(axis->encoder_.pos_estimate_))); + + uint32_t floatBytes; + static_assert(sizeof axis->sensorless_estimator_.pll_pos_ == sizeof floatBytes); + std::memcpy(&floatBytes, &axis->sensorless_estimator_.pll_pos_, sizeof floatBytes); + + txmsg.buf[0] = floatBytes; + txmsg.buf[1] = floatBytes >> 8; + txmsg.buf[2] = floatBytes >> 16; + txmsg.buf[3] = floatBytes >> 24; + + static_assert(sizeof floatBytes == sizeof axis->sensorless_estimator_.vel_estimate_); + std::memcpy(&floatBytes, &axis->sensorless_estimator_.vel_estimate_, sizeof floatBytes); + txmsg.buf[4] = floatBytes; + txmsg.buf[5] = floatBytes >> 8; + txmsg.buf[6] = floatBytes >> 16; + txmsg.buf[7] = floatBytes >> 24; + + odCAN->write(txmsg); + } +} + +void CANSimple::get_encoder_count_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_ENCODER_COUNT; + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + txmsg.buf[0] = axis->encoder_.shadow_count_; + txmsg.buf[1] = axis->encoder_.shadow_count_ >> 8; + txmsg.buf[2] = axis->encoder_.shadow_count_ >> 16; + txmsg.buf[3] = axis->encoder_.shadow_count_ >> 24; + + txmsg.buf[4] = axis->encoder_.count_in_cpr_; + txmsg.buf[5] = axis->encoder_.count_in_cpr_ >> 8; + txmsg.buf[6] = axis->encoder_.count_in_cpr_ >> 16; + txmsg.buf[7] = axis->encoder_.count_in_cpr_ >> 24; + + odCAN->write(txmsg); + } +} + +void CANSimple::set_input_pos_callback(Axis* axis, can_Message_t& msg) { + axis->controller_.input_pos_ = can_getSignal(msg, 0, 32, true); + axis->controller_.input_vel_ = can_getSignal(msg, 32, 16, true, 0.001f, 0); + axis->controller_.input_torque_ = can_getSignal(msg, 48, 16, true, 0.001f, 0); + axis->controller_.input_pos_updated(); +} + +void CANSimple::set_input_vel_callback(Axis* axis, can_Message_t& msg) { + axis->controller_.input_vel_ = can_getSignal(msg, 0, 32, true); + axis->controller_.input_torque_ = can_getSignal(msg, 32, 32, true); +} + +void CANSimple::set_input_torque_callback(Axis* axis, can_Message_t& msg) { + axis->controller_.input_torque_ = can_getSignal(msg, 0, 32, true); +} + +void CANSimple::set_controller_modes_callback(Axis* axis, can_Message_t& msg) { + axis->controller_.config_.control_mode = static_cast(can_getSignal(msg, 0, 32, true)); + axis->controller_.config_.input_mode = static_cast(can_getSignal(msg, 32, 32, true)); +} + +void CANSimple::set_vel_limit_callback(Axis* axis, can_Message_t& msg) { + axis->controller_.config_.vel_limit = can_getSignal(msg, 0, 32, true); +} + +void CANSimple::start_anticogging_callback(Axis* axis, can_Message_t& msg) { + axis->controller_.start_anticogging_calibration(); +} + +void CANSimple::set_traj_vel_limit_callback(Axis* axis, can_Message_t& msg) { + axis->trap_traj_.config_.vel_limit = can_getSignal(msg, 0, 32, true); +} + +void CANSimple::set_traj_accel_limits_callback(Axis* axis, can_Message_t& msg) { + axis->trap_traj_.config_.accel_limit = can_getSignal(msg, 0, 32, true); + axis->trap_traj_.config_.decel_limit = can_getSignal(msg, 32, 32, true); +} + +void CANSimple::set_traj_inertia_callback(Axis* axis, can_Message_t& msg) { + axis->controller_.config_.inertia = can_getSignal(msg, 0, 32, true); +} + +void CANSimple::get_iq_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_IQ; + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + uint32_t floatBytes; + static_assert(sizeof axis->motor_.current_control_.Iq_setpoint == sizeof floatBytes); + std::memcpy(&floatBytes, &axis->motor_.current_control_.Iq_setpoint, sizeof floatBytes); + + txmsg.buf[0] = floatBytes; + txmsg.buf[1] = floatBytes >> 8; + txmsg.buf[2] = floatBytes >> 16; + txmsg.buf[3] = floatBytes >> 24; + + static_assert(sizeof floatBytes == sizeof axis->motor_.current_control_.Iq_measured); + std::memcpy(&floatBytes, &axis->motor_.current_control_.Iq_measured, sizeof floatBytes); + txmsg.buf[4] = floatBytes; + txmsg.buf[5] = floatBytes >> 8; + txmsg.buf[6] = floatBytes >> 16; + txmsg.buf[7] = floatBytes >> 24; + + odCAN->write(txmsg); + } +} + +void CANSimple::get_vbus_voltage_callback(Axis* axis, can_Message_t& msg) { + if (msg.rtr) { + can_Message_t txmsg; + + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_GET_VBUS_VOLTAGE; + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + uint32_t floatBytes; + static_assert(sizeof vbus_voltage == sizeof floatBytes); + std::memcpy(&floatBytes, &vbus_voltage, sizeof floatBytes); + + // This also works in principle, but I don't have hardware to verify endianness + // std::memcpy(&txmsg.buf[0], &vbus_voltage, sizeof vbus_voltage); + + txmsg.buf[0] = floatBytes; + txmsg.buf[1] = floatBytes >> 8; + txmsg.buf[2] = floatBytes >> 16; + txmsg.buf[3] = floatBytes >> 24; + + txmsg.buf[4] = 0; + txmsg.buf[5] = 0; + txmsg.buf[6] = 0; + txmsg.buf[7] = 0; + + odCAN->write(txmsg); + } +} + +void CANSimple::clear_errors_callback(Axis* axis, can_Message_t& msg) { + axis->clear_errors(); +} + +void CANSimple::send_heartbeat(Axis* axis) { + can_Message_t txmsg; + txmsg.id = axis->config_.can_node_id << NUM_CMD_ID_BITS; + txmsg.id += MSG_ODRIVE_HEARTBEAT; // heartbeat ID + txmsg.isExt = axis->config_.can_node_id_extended; + txmsg.len = 8; + + // Axis errors in 1st 32-bit value + txmsg.buf[0] = axis->error_; + txmsg.buf[1] = axis->error_ >> 8; + txmsg.buf[2] = axis->error_ >> 16; + txmsg.buf[3] = axis->error_ >> 24; + + // Current state of axis in 2nd 32-bit value + txmsg.buf[4] = axis->current_state_; + txmsg.buf[5] = axis->current_state_ >> 8; + txmsg.buf[6] = axis->current_state_ >> 16; + txmsg.buf[7] = axis->current_state_ >> 24; + odCAN->write(txmsg); +} + +uint32_t CANSimple::get_node_id(uint32_t msgID) { + return (msgID >> NUM_CMD_ID_BITS); // Upper 6 or more bits +} + +uint8_t CANSimple::get_cmd_id(uint32_t msgID) { + return (msgID & 0x01F); // Bottom 5 bits +} \ No newline at end of file diff --git a/Firmware/communication/can_simple.hpp b/Firmware/communication/can_simple.hpp new file mode 100644 index 00000000..0168b978 --- /dev/null +++ b/Firmware/communication/can_simple.hpp @@ -0,0 +1,84 @@ +#ifndef __CAN_SIMPLE_HPP_ +#define __CAN_SIMPLE_HPP_ + +#include "interface_can.hpp" + +class CANSimple { + public: + enum { + MSG_CO_NMT_CTRL = 0x000, // CANOpen NMT Message REC + MSG_ODRIVE_HEARTBEAT, + MSG_ODRIVE_ESTOP, + MSG_GET_MOTOR_ERROR, // Errors + MSG_GET_ENCODER_ERROR, + MSG_GET_SENSORLESS_ERROR, + MSG_SET_AXIS_NODE_ID, + MSG_SET_AXIS_REQUESTED_STATE, + MSG_SET_AXIS_STARTUP_CONFIG, + MSG_GET_ENCODER_ESTIMATES, + MSG_GET_ENCODER_COUNT, + MSG_SET_CONTROLLER_MODES, + MSG_SET_INPUT_POS, + MSG_SET_INPUT_VEL, + MSG_SET_INPUT_TORQUE, + MSG_SET_VEL_LIMIT, + MSG_START_ANTICOGGING, + MSG_SET_TRAJ_VEL_LIMIT, + MSG_SET_TRAJ_ACCEL_LIMITS, + MSG_SET_TRAJ_INERTIA, + MSG_GET_IQ, + MSG_GET_SENSORLESS_ESTIMATES, + MSG_RESET_ODRIVE, + MSG_GET_VBUS_VOLTAGE, + MSG_CLEAR_ERRORS, + MSG_CO_HEARTBEAT_CMD = 0x700, // CANOpen NMT Heartbeat SEND + }; + + static void handle_can_message(can_Message_t& msg); + static void send_heartbeat(Axis* axis); + + private: + static void nmt_callback(Axis* axis, can_Message_t& msg); + static void estop_callback(Axis* axis, can_Message_t& msg); + static void get_motor_error_callback(Axis* axis, can_Message_t& msg); + static void get_encoder_error_callback(Axis* axis, can_Message_t& msg); + static void get_controller_error_callback(Axis* axis, can_Message_t& msg); + static void get_sensorless_error_callback(Axis* axis, can_Message_t& msg); + static void set_axis_nodeid_callback(Axis* axis, can_Message_t& msg); + static void set_axis_requested_state_callback(Axis* axis, can_Message_t& msg); + static void set_axis_startup_config_callback(Axis* axis, can_Message_t& msg); + static void get_encoder_estimates_callback(Axis* axis, can_Message_t& msg); + static void get_encoder_count_callback(Axis* axis, can_Message_t& msg); + static void set_input_pos_callback(Axis* axis, can_Message_t& msg); + static void set_input_vel_callback(Axis* axis, can_Message_t& msg); + static void set_input_torque_callback(Axis* axis, can_Message_t& msg); + static void set_controller_modes_callback(Axis* axis, can_Message_t& msg); + static void set_vel_limit_callback(Axis* axis, can_Message_t& msg); + static void start_anticogging_callback(Axis* axis, can_Message_t& msg); + static void set_traj_vel_limit_callback(Axis* axis, can_Message_t& msg); + static void set_traj_accel_limits_callback(Axis* axis, can_Message_t& msg); + static void set_traj_inertia_callback(Axis* axis, can_Message_t& msg); + static void get_iq_callback(Axis* axis, can_Message_t& msg); + static void get_sensorless_estimates_callback(Axis* axis, can_Message_t& msg); + static void get_vbus_voltage_callback(Axis* axis, can_Message_t& msg); + static void clear_errors_callback(Axis* axis, can_Message_t& msg); + + // Utility functions + static uint32_t get_node_id(uint32_t msgID); + static uint8_t get_cmd_id(uint32_t msgID); + + // Fetch a specific signal from the message + + // This functional way of handling the messages is neat and is much cleaner from + // a data security point of view, but it will require some tweaking + // + // const std::map> callback_map = { + // {0x000, std::bind(&CANSimple::heartbeat_callback, this, _1)} + // }; +}; + + + + + +#endif \ No newline at end of file diff --git a/Firmware/communication/communication.cpp b/Firmware/communication/communication.cpp index 79982e64..c9eaa589 100644 --- a/Firmware/communication/communication.cpp +++ b/Firmware/communication/communication.cpp @@ -10,9 +10,8 @@ #include "odrive_main.h" #include "freertos_vars.h" -#include "utils.h" - -#include "../build/version.h" // autogenerated based on Git state +#include "utils.hpp" +#include "gpio_utils.hpp" #include #include @@ -35,178 +34,41 @@ char serial_number_str[13]; // 12 digits + null termination /* Private constant data -----------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ -#if HW_VERSION_MAJOR == 3 -// Determine start address of the OTP struct: -// The OTP is organized into 16-byte blocks. -// If the first block starts with "0xfe" we use the first block. -// If the first block starts with "0x00" and the second block starts with "0xfe", -// we use the second block. This gives the user the chance to screw up once. -// If none of the above is the case, we consider the OTP invalid (otp_ptr will be NULL). -const uint8_t* otp_ptr = - (*(uint8_t*)FLASH_OTP_BASE == 0xfe) ? (uint8_t*)FLASH_OTP_BASE : - (*(uint8_t*)FLASH_OTP_BASE != 0x00) ? NULL : - (*(uint8_t*)(FLASH_OTP_BASE + 0x10) != 0xfe) ? NULL : - (uint8_t*)(FLASH_OTP_BASE + 0x10); - -// Read hardware version from OTP if available, otherwise fall back -// to software defined version. -const uint8_t hw_version_major = otp_ptr ? otp_ptr[3] : HW_VERSION_MAJOR; -const uint8_t hw_version_minor = otp_ptr ? otp_ptr[4] : HW_VERSION_MINOR; -const uint8_t hw_version_variant = otp_ptr ? otp_ptr[5] : HW_VERSION_VOLTAGE; -#else -#error "not implemented" -#endif - -// the corresponding macros are defined in the autogenerated version.h -const uint8_t fw_version_major = FW_VERSION_MAJOR; -const uint8_t fw_version_minor = FW_VERSION_MINOR; -const uint8_t fw_version_revision = FW_VERSION_REVISION; -const uint8_t fw_version_unreleased = FW_VERSION_UNRELEASED; // 0 for official releases, 1 otherwise - osThreadId comm_thread; +const uint32_t stack_size_comm_thread = 4096; // Bytes volatile bool endpoint_list_valid = false; -static uint32_t test_property = 0; - /* Private function prototypes -----------------------------------------------*/ - -auto make_protocol_definitions(PWMMapping_t& mapping) { - return make_protocol_member_list( - make_protocol_property("endpoint", &mapping.endpoint), - make_protocol_property("min", &mapping.min), - make_protocol_property("max", &mapping.max) - ); -} - /* Function implementations --------------------------------------------------*/ void init_communication(void) { printf("hi!\r\n"); // Start command handling thread - osThreadDef(task_cmd_parse, communication_task, osPriorityNormal, 0, 8000 /* in 32-bit words */); // TODO: fix stack issues + osThreadDef(task_cmd_parse, communication_task, osPriorityNormal, 0, stack_size_comm_thread / sizeof(StackType_t)); comm_thread = osThreadCreate(osThread(task_cmd_parse), NULL); while (!endpoint_list_valid) osDelay(1); } - float oscilloscope[OSCILLOSCOPE_SIZE] = {0}; size_t oscilloscope_pos = 0; - -static CAN_context can1_ctx; - -// Helper class because the protocol library doesn't yet -// support non-member functions -// TODO: make this go away -class StaticFunctions { -public: - void save_configuration_helper() { save_configuration(); } - void erase_configuration_helper() { erase_configuration(); } - void NVIC_SystemReset_helper() { NVIC_SystemReset(); } - void enter_dfu_mode_helper() { enter_dfu_mode(); } - float get_oscilloscope_val(uint32_t index) { return oscilloscope[index]; } - float get_adc_voltage_(uint32_t gpio) { return get_adc_voltage(get_gpio_port_by_pin(gpio), get_gpio_pin_by_pin(gpio)); } - int32_t test_function(int32_t delta) { static int cnt = 0; return cnt += delta; } -} static_functions; - -// When adding new functions/variables to the protocol, be careful not to -// blow the communication stack. You can check comm_stack_info to see -// how much headroom you have. -static inline auto make_obj_tree() { - return make_protocol_member_list( - make_protocol_ro_property("vbus_voltage", &vbus_voltage), - make_protocol_ro_property("serial_number", &serial_number), - make_protocol_ro_property("hw_version_major", &hw_version_major), - make_protocol_ro_property("hw_version_minor", &hw_version_minor), - make_protocol_ro_property("hw_version_variant", &hw_version_variant), - make_protocol_ro_property("fw_version_major", &fw_version_major), - make_protocol_ro_property("fw_version_minor", &fw_version_minor), - make_protocol_ro_property("fw_version_revision", &fw_version_revision), - make_protocol_ro_property("fw_version_unreleased", &fw_version_unreleased), - make_protocol_ro_property("user_config_loaded", const_cast(&user_config_loaded_)), - make_protocol_ro_property("brake_resistor_armed", &brake_resistor_armed), - make_protocol_object("system_stats", - make_protocol_ro_property("uptime", &system_stats_.uptime), - make_protocol_ro_property("min_heap_space", &system_stats_.min_heap_space), - make_protocol_ro_property("min_stack_space_axis0", &system_stats_.min_stack_space_axis0), - make_protocol_ro_property("min_stack_space_axis1", &system_stats_.min_stack_space_axis1), - make_protocol_ro_property("min_stack_space_comms", &system_stats_.min_stack_space_comms), - make_protocol_ro_property("min_stack_space_usb", &system_stats_.min_stack_space_usb), - make_protocol_ro_property("min_stack_space_uart", &system_stats_.min_stack_space_uart), - make_protocol_ro_property("min_stack_space_usb_irq", &system_stats_.min_stack_space_usb_irq), - make_protocol_ro_property("min_stack_space_startup", &system_stats_.min_stack_space_startup), - make_protocol_object("usb", - make_protocol_ro_property("rx_cnt", &usb_stats_.rx_cnt), - make_protocol_ro_property("tx_cnt", &usb_stats_.tx_cnt), - make_protocol_ro_property("tx_overrun_cnt", &usb_stats_.tx_overrun_cnt) - ), - make_protocol_object("i2c", - make_protocol_ro_property("addr", &i2c_stats_.addr), - make_protocol_ro_property("addr_match_cnt", &i2c_stats_.addr_match_cnt), - make_protocol_ro_property("rx_cnt", &i2c_stats_.rx_cnt), - make_protocol_ro_property("error_cnt", &i2c_stats_.error_cnt) - ) - ), - make_protocol_object("config", - make_protocol_property("brake_resistance", &board_config.brake_resistance), - // TODO: changing this currently requires a reboot - fix this - make_protocol_property("enable_uart", &board_config.enable_uart), - make_protocol_property("enable_i2c_instead_of_can" , &board_config.enable_i2c_instead_of_can), // requires a reboot - make_protocol_property("enable_ascii_protocol_on_usb", &board_config.enable_ascii_protocol_on_usb), - make_protocol_property("dc_bus_undervoltage_trip_level", &board_config.dc_bus_undervoltage_trip_level), - make_protocol_property("dc_bus_overvoltage_trip_level", &board_config.dc_bus_overvoltage_trip_level), -#if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 - make_protocol_object("gpio1_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[0])), - make_protocol_object("gpio2_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[1])), - make_protocol_object("gpio3_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[2])), -#endif - make_protocol_object("gpio4_pwm_mapping", make_protocol_definitions(board_config.pwm_mappings[3])), - - make_protocol_object("gpio3_analog_mapping", make_protocol_definitions(board_config.analog_mappings[2])), - make_protocol_object("gpio4_analog_mapping", make_protocol_definitions(board_config.analog_mappings[3])) - ), - make_protocol_object("axis0", axes[0]->make_protocol_definitions()), - make_protocol_object("axis1", axes[1]->make_protocol_definitions()), - make_protocol_object("can", can1_ctx.make_protocol_definitions()), - make_protocol_property("test_property", &test_property), - make_protocol_function("test_function", static_functions, &StaticFunctions::test_function, "delta"), - make_protocol_function("get_oscilloscope_val", static_functions, &StaticFunctions::get_oscilloscope_val, "index"), - make_protocol_function("get_adc_voltage", static_functions, &StaticFunctions::get_adc_voltage_, "gpio"), - make_protocol_function("save_configuration", static_functions, &StaticFunctions::save_configuration_helper), - make_protocol_function("erase_configuration", static_functions, &StaticFunctions::erase_configuration_helper), - make_protocol_function("reboot", static_functions, &StaticFunctions::NVIC_SystemReset_helper), - make_protocol_function("enter_dfu_mode", static_functions, &StaticFunctions::enter_dfu_mode_helper) - ); -} - -using tree_type = decltype(make_obj_tree()); -uint8_t tree_buffer[sizeof(tree_type)]; - - // Thread to handle deffered processing of USB interrupt, and // read commands out of the UART DMA circular buffer void communication_task(void * ctx) { (void) ctx; // unused parameter - // TODO: this is supposed to use the move constructor, but currently - // the compiler uses the copy-constructor instead. Thus the make_obj_tree - // ends up with a stupid stack size of around 8000 bytes. Fix this. - auto tree_ptr = new (tree_buffer) tree_type(make_obj_tree()); - fibre_publish(*tree_ptr); - // Allow main init to continue endpoint_list_valid = true; start_uart_server(); start_usb_server(); - if (board_config.enable_i2c_instead_of_can) { + if (odrv.config_.enable_i2c_instead_of_can) { start_i2c_server(); } else { - // TODO: finish implementing CAN - // start_can_server(can1_ctx, CAN1, serial_number); + odCAN->start_can_server(); } for (;;) { @@ -228,3 +90,9 @@ int _write(int file, const char* data, int len) { #endif return len; } + + +#include "../autogen/function_stubs.hpp" + +ODrive& ep_root = odrv; +#include "../autogen/endpoints.hpp" diff --git a/Firmware/communication/communication.h b/Firmware/communication/communication.h index 8b38e6b4..6987aa11 100644 --- a/Firmware/communication/communication.h +++ b/Firmware/communication/communication.h @@ -15,12 +15,10 @@ extern "C" { #include extern osThreadId comm_thread; - -extern const uint8_t hw_version_major; -extern const uint8_t hw_version_minor; -extern const uint8_t hw_version_variant; +extern const uint32_t stack_size_comm_thread; void init_communication(void); +void initTree(); void communication_task(void * ctx); #ifdef __cplusplus diff --git a/Firmware/communication/interface_can.cpp b/Firmware/communication/interface_can.cpp index 3bf822bd..20b8ea6a 100644 --- a/Firmware/communication/interface_can.cpp +++ b/Firmware/communication/interface_can.cpp @@ -1,297 +1,214 @@ -/* -* -* Zero-config node ID negotiation -* ------------------------------- -* -* A heartbeat message is a message with a 8 byte unique serial number as payload. -* A regular message is any message that is not a heartbeat message. -* -* All nodes MUST obey these four rules: -* -* a) At a given point in time, a node MUST consider a node ID taken (by others) -* if any of the following is true: -* - the node received a (not self-emitted) heartbeat message with that node ID -* within the last second -* - the node attempted and failed at sending a heartbeat message with that -* node ID within the last second (failed in the sense of not ACK'd) -* -* b) At a given point in time, a node MUST NOT consider a node ID self-assigned -* if, within the last second, it did not succeed in sending a heartbeat -* message with that node ID. -* -* c) At a given point in time, a node MUST NOT send any heartbeat message with -* a node ID that is taken. -* -* d) At a given point in time, a node MUST NOT send any regular message with -* a node ID that is not self-assigned. -* -* Hardware allocation -* ------------------- -* RX FIFO0: -* - filter bank 0: heartbeat messages -*/ - #include "interface_can.hpp" + #include "fibre/crc.hpp" -#include "utils.h" +#include "freertos_vars.h" +#include "utils.hpp" #include -#include #include +#include -#define CAN_HEARTBEAT_INTERVAL 1000 // [ms] -#define CAN_HEARTBEAT_MARGIN 10 // maximum time that a heartbeat message can be delayed until we stop sending other messages [ms] +// Specific CAN Protocols +#include "can_simple.hpp" -// defined in can.c -extern CAN_HandleTypeDef hcan1; -extern CAN_HandleTypeDef hcan2; -extern CAN_HandleTypeDef hcan3; +// Safer context handling via maps instead of arrays +// #include +// std::unordered_map ctxMap; -static CAN_context* ctxs[3] = { nullptr, nullptr, nullptr }; - -struct CAN_context* get_can_ctx(CAN_HandleTypeDef *hcan) { -#if defined(CAN1) - if (hcan->Instance == CAN1) return ctxs[0]; -#endif -#if defined(CAN2) - if (hcan->Instance == CAN2) return ctxs[1]; -#endif -#if defined(CAN3) - if (hcan->Instance == CAN3) return ctxs[2]; -#endif - return nullptr; +// Constructor is called by communication.cpp and the handle is assigned appropriately +ODriveCAN::ODriveCAN(ODriveCAN::Config_t &config, CAN_HandleTypeDef *handle) + : config_{config}, + handle_{handle} { + // ctxMap[handle_] = this; } - -void consider_node_id_in_use(CAN_context* ctx, uint8_t node_id) { - ctx->node_ids_in_use_0[node_id >> 5] |= (1 << (node_id & 0x1f)); -} - -bool is_node_id_in_use(CAN_context* ctx, uint32_t node_id) { - if (node_id == 0) // node ID 0 is reserved (is it though?) - return true; - return (ctx->node_ids_in_use_0[node_id >> 5] & (1 << (node_id & 0x1f))) - || (ctx->node_ids_in_use_1[node_id >> 5] & (1 << (node_id & 0x1f))); -} - -bool select_another_node_id(CAN_context* ctx) { - ctx->node_id_expiry = osKernelSysTick() - 1; - - // Find a new node ID that is not in use - for (uint8_t i = 0; i < 32; i++) { - // Each time we select a new node ID, we use the next byte from the serial - // number to get advance the node ID. - uint8_t poor_mans_random_byte = ((uint8_t*)ctx->serial_number)[ctx->node_id_rng_state]; - if (++(ctx->node_id_rng_state) >= sizeof(ctx->serial_number)) - ctx->node_id_rng_state = 0; - ctx->node_id = calc_crc(ctx->node_id, poor_mans_random_byte); - if (!is_node_id_in_use(ctx, ctx->node_id)) - return true; - } - return false; -} - - -void server_thread(CAN_context* ctx) { - uint32_t next_1s_tick = osKernelSysTick() + 1000; +void ODriveCAN::can_server_thread() { for (;;) { - if (deadline_to_timeout(next_1s_tick) == 0) - - // wait until either the next heartbeat is due or a hearbeat was requested - // by releasing the semaphore - osSemaphoreWait(ctx->sem_send_heartbeat, deadline_to_timeout(next_1s_tick)); - if (!is_in_the_future(next_1s_tick)) - memcpy(ctx->node_ids_in_use_1, ctx->node_ids_in_use_0, sizeof(ctx->node_ids_in_use_1)); - next_1s_tick += 1000; - if (!is_in_the_future(next_1s_tick)) - next_1s_tick = osKernelSysTick(); // fast-forward if we missed several 1 second ticks + uint32_t status = HAL_CAN_GetError(handle_); + if (status == HAL_CAN_ERROR_NONE) { + can_Message_t rxmsg; - if (is_node_id_in_use(ctx, ctx->node_id)) { - if (!select_another_node_id(ctx)) - continue; - else - next_1s_tick += ctx->node_id; // shift the 1s tick by a bit + osSemaphoreWait(sem_can, 10); // Poll every 10ms regardless of sempahore status + while (available()) { + read(rxmsg); + switch (config_.protocol) { + case PROTOCOL_SIMPLE: + CANSimple::handle_can_message(rxmsg); + break; + } + } + HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING); + } else { + if (status == HAL_CAN_ERROR_TIMEOUT) { + HAL_CAN_ResetError(handle_); + status = HAL_CAN_Start(handle_); + if (status == HAL_OK) + status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING); + } } - - uint8_t data[8]; - //uint8_t data[] = { ctx->node_id }; // this would be the correct data for CANopen - TODO: make it compatible - *(uint64_t*)data = ctx->serial_number; - - CAN_TxHeaderTypeDef header = { - .StdId = 0x700u + ctx->node_id, - .ExtId = 0, - .IDE = CAN_ID_STD, - .RTR = CAN_RTR_DATA, - .DLC = sizeof(data), - .TransmitGlobalTime = DISABLE - }; - HAL_CAN_AddTxMessage(ctx->handle, &header, data, &ctx->last_heartbeat_mailbox); } } -bool start_can_server(CAN_context& ctx, CAN_TypeDef *port, uint64_t serial_number) { - //MX_CAN1_Init(); // TODO: flatten -#if defined(CAN1) - if (port == CAN1) ctx.handle = &hcan1, ctxs[0] = &ctx; else -#endif -#if defined(CAN2) - // TODO: move CubeMX stuff into this file so all symbols are defined - //if (port == CAN2) ctx.handle = &hcan2, ctxs[1] = &ctx; else -#endif -#if defined(CAN3) - if (port == CAN3) ctx.handle = &hcan3, ctxs[2] = &ctx; else -#endif - return false; // fail if none of the above checks matched +static void can_server_thread_wrapper(void *ctx) { + reinterpret_cast(ctx)->can_server_thread(); + reinterpret_cast(ctx)->thread_id_valid_ = false; +} +bool ODriveCAN::start_can_server() { HAL_StatusTypeDef status; - ctx.node_id = calc_crc(0, (const uint8_t*)UID_BASE, 12); - ctx.serial_number = serial_number; - osSemaphoreDef(sem_send_heartbeat); - ctx.sem_send_heartbeat = osSemaphoreCreate(osSemaphore(sem_send_heartbeat), 1); - osSemaphoreWait(ctx.sem_send_heartbeat, 0); + set_baud_rate(config_.baud_rate); - //// Set up heartbeat filter - CAN_FilterTypeDef sFilterConfig = { - .FilterIdHigh = ((0x700u + ctx.node_id) << 5) | (0x0 << 2), // own heartbeat (standard ID, no RTR) - .FilterIdLow = (0x700u << 5) | (0x0 << 2), // any heartbeat (standard ID, no RTR) - .FilterMaskIdHigh = (0x7ffu << 5) | (0x3 << 2), - .FilterMaskIdLow = (0x780u << 5) | (0x3 << 2), - .FilterFIFOAssignment = CAN_RX_FIFO0, - .FilterBank = 0, - .FilterMode = CAN_FILTERMODE_IDMASK, - .FilterScale = CAN_FILTERSCALE_16BIT, // two 16-bit filters - .FilterActivation = ENABLE, - .SlaveStartFilterBank = 0 - }; - status = HAL_CAN_ConfigFilter(ctx.handle, &sFilterConfig); - if (status != HAL_OK) - return false; + status = HAL_CAN_Init(handle_); - status = HAL_CAN_Start(ctx.handle); - if (status != HAL_OK) - return false; + CAN_FilterTypeDef filter; + filter.FilterActivation = ENABLE; + filter.FilterBank = 0; + filter.FilterFIFOAssignment = CAN_RX_FIFO0; + filter.FilterIdHigh = 0x0000; + filter.FilterIdLow = 0x0000; + filter.FilterMaskIdHigh = 0x0000; + filter.FilterMaskIdLow = 0x0000; + filter.FilterMode = CAN_FILTERMODE_IDMASK; + filter.FilterScale = CAN_FILTERSCALE_32BIT; - status = HAL_CAN_ActivateNotification(ctx.handle, - CAN_IT_TX_MAILBOX_EMPTY | - CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_RX_FIFO1_MSG_PENDING | /* we probably only want this */ - CAN_IT_RX_FIFO0_FULL | CAN_IT_RX_FIFO1_FULL | - CAN_IT_RX_FIFO0_OVERRUN | CAN_IT_RX_FIFO1_OVERRUN | - CAN_IT_WAKEUP | CAN_IT_SLEEP_ACK | - CAN_IT_ERROR_WARNING | CAN_IT_ERROR_PASSIVE | - CAN_IT_BUSOFF | CAN_IT_LAST_ERROR_CODE | - CAN_IT_ERROR); - if (status != HAL_OK) - return false; - - server_thread(&ctx); - return true; + status = HAL_CAN_ConfigFilter(handle_, &filter); + + status = HAL_CAN_Start(handle_); + if (status == HAL_OK) + status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING); + + osThreadDef(can_server_thread_def, can_server_thread_wrapper, osPriorityNormal, 0, stack_size_ / sizeof(StackType_t)); + thread_id_ = osThreadCreate(osThread(can_server_thread_def), this); + thread_id_valid_ = true; + + return status; } -void tx_complete_callback(CAN_HandleTypeDef *hcan, uint8_t mailbox_idx) { - CAN_context *ctx = get_can_ctx(hcan); - if (!ctx) return; - ctx->tx_msg_cnt++; - if (mailbox_idx == ctx->last_heartbeat_mailbox) { - // we succeeded in sending a heartbeat - // now we're allowed to send messages for the next second plus a small margin - ctx->node_id_expiry = osKernelSysTick() + CAN_HEARTBEAT_INTERVAL + CAN_HEARTBEAT_MARGIN; - } -} +// Send a CAN message on the bus +uint32_t ODriveCAN::write(can_Message_t &txmsg) { + if (HAL_CAN_GetError(handle_) == HAL_CAN_ERROR_NONE) { + CAN_TxHeaderTypeDef header; + header.StdId = txmsg.id; + header.ExtId = txmsg.id; + header.IDE = txmsg.isExt ? CAN_ID_EXT : CAN_ID_STD; + header.RTR = CAN_RTR_DATA; + header.DLC = txmsg.len; + header.TransmitGlobalTime = FunctionalState::DISABLE; -void tx_aborted_callback(CAN_HandleTypeDef *hcan, uint8_t mailbox_idx) { - //__asm volatile ("bkpt"); - if (!get_can_ctx(hcan)) - return; - get_can_ctx(hcan)->TxMailboxAbortCallbackCnt++; -} + uint32_t retTxMailbox = 0; + if (HAL_CAN_GetTxMailboxesFreeLevel(handle_) > 0) + HAL_CAN_AddTxMessage(handle_, &header, txmsg.buf, &retTxMailbox); -void tx_error(CAN_context *ctx, uint8_t mailbox_idx) { - if (mailbox_idx == ctx->last_heartbeat_mailbox) { - // Consider the node ID in use - consider_node_id_in_use(ctx, ctx->node_id); - // Try to find a new node ID that is not in use and immediately - // resend heartbeat if we find one - if (select_another_node_id(ctx)) - osSemaphoreRelease(ctx->sem_send_heartbeat); - } -} - -void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) { tx_complete_callback(hcan, 0); } -void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan) { tx_complete_callback(hcan, 1); } -void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan) { tx_complete_callback(hcan, 2); } -void HAL_CAN_TxMailbox0AbortCallback(CAN_HandleTypeDef *hcan) { tx_aborted_callback(hcan, 0); } -void HAL_CAN_TxMailbox1AbortCallback(CAN_HandleTypeDef *hcan) { tx_aborted_callback(hcan, 1); } -void HAL_CAN_TxMailbox2AbortCallback(CAN_HandleTypeDef *hcan) { tx_aborted_callback(hcan, 2); } - -void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { - CAN_context *ctx = get_can_ctx(hcan); - if (!ctx) return; - ctx->received_msg_cnt++; - - CAN_RxHeaderTypeDef header; - uint8_t data[8]; - HAL_StatusTypeDef status = HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header, data); - if (status != HAL_OK) { - ctx->unexpected_errors++; - return; - } - - uint8_t node_id = header.StdId & 0x07fu; - if ((header.StdId & 0x780u) == 0x700u) { - ctx->received_ack++; - consider_node_id_in_use(ctx, node_id); + return retTxMailbox; } else { - ctx->unhandled_messages++; + return -1; } } -void HAL_CAN_RxFifo0FullCallback(CAN_HandleTypeDef *hcan) { if (get_can_ctx(hcan)) get_can_ctx(hcan)->RxFifo0FullCallbackCnt++; } +uint32_t ODriveCAN::available() { + return (HAL_CAN_GetRxFifoFillLevel(handle_, CAN_RX_FIFO0) + HAL_CAN_GetRxFifoFillLevel(handle_, CAN_RX_FIFO1)); +} -void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan) { if (get_can_ctx(hcan)) get_can_ctx(hcan)->RxFifo1MsgPendingCallbackCnt++; } -void HAL_CAN_RxFifo1FullCallback(CAN_HandleTypeDef *hcan) { if (get_can_ctx(hcan)) get_can_ctx(hcan)->RxFifo1FullCallbackCnt++; } -void HAL_CAN_SleepCallback(CAN_HandleTypeDef *hcan) { if (get_can_ctx(hcan)) get_can_ctx(hcan)->SleepCallbackCnt++; } -void HAL_CAN_WakeUpFromRxMsgCallback(CAN_HandleTypeDef *hcan) { if (get_can_ctx(hcan)) get_can_ctx(hcan)->WakeUpFromRxMsgCallbackCnt++; } +bool ODriveCAN::read(can_Message_t &rxmsg) { + CAN_RxHeaderTypeDef header; + bool validRead = false; + if (HAL_CAN_GetRxFifoFillLevel(handle_, CAN_RX_FIFO0) > 0) { + HAL_CAN_GetRxMessage(handle_, CAN_RX_FIFO0, &header, rxmsg.buf); + validRead = true; + } else if (HAL_CAN_GetRxFifoFillLevel(handle_, CAN_RX_FIFO1) > 0) { + HAL_CAN_GetRxMessage(handle_, CAN_RX_FIFO1, &header, rxmsg.buf); + validRead = true; + } + rxmsg.isExt = header.IDE; + rxmsg.id = rxmsg.isExt ? header.ExtId : header.StdId; // If it's an extended message, pass the extended ID + rxmsg.len = header.DLC; + rxmsg.rtr = header.RTR; + + return validRead; +} + +// Set one of only a few common baud rates. CAN doesn't do arbitrary baud rates well due to the time-quanta issue. +// 21 TQ allows for easy sampling at exactly 80% (recommended by Vector Informatik GmbH for high reliability systems) +// Conveniently, the CAN peripheral's 42MHz clock lets us easily create 21TQs for all common baud rates +void ODriveCAN::set_baud_rate(uint32_t baudRate) { + switch (baudRate) { + case CAN_BAUD_125K: + handle_->Init.Prescaler = 16; // 21 TQ's + config_.baud_rate = baudRate; + reinit_can(); + break; + + case CAN_BAUD_250K: + handle_->Init.Prescaler = 8; // 21 TQ's + config_.baud_rate = baudRate; + reinit_can(); + break; + + case CAN_BAUD_500K: + handle_->Init.Prescaler = 4; // 21 TQ's + config_.baud_rate = baudRate; + reinit_can(); + break; + + case CAN_BAUD_1000K: + handle_->Init.Prescaler = 2; // 21 TQ's + config_.baud_rate = baudRate; + reinit_can(); + break; + + default: + // baudRate is invalid, so don't accept it. + break; + } +} + +void ODriveCAN::reinit_can() { + HAL_CAN_Stop(handle_); + HAL_CAN_Init(handle_); + auto status = HAL_CAN_Start(handle_); + if (status == HAL_OK) + status = HAL_CAN_ActivateNotification(handle_, CAN_IT_RX_FIFO0_MSG_PENDING); +} + +void ODriveCAN::set_error(Error error) { + error_ |= error; +} +// This function is called by each axis. +// It provides an abstraction from the specific CAN protocol in use +void ODriveCAN::send_heartbeat(Axis *axis) { + // Handle heartbeat message + if (axis->config_.can_heartbeat_rate_ms > 0) { + uint32_t now = osKernelSysTick(); + if ((now - axis->last_heartbeat_) >= axis->config_.can_heartbeat_rate_ms) { + switch (config_.protocol) { + case PROTOCOL_SIMPLE: + CANSimple::send_heartbeat(axis); + break; + } + axis->last_heartbeat_ = now; + } + } +} + +void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_TxMailbox0AbortCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_TxMailbox1AbortCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_TxMailbox2AbortCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { + HAL_CAN_DeactivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING); + osSemaphoreRelease(sem_can); +} +void HAL_CAN_RxFifo0FullCallback(CAN_HandleTypeDef *hcan) { + // osSemaphoreRelease(sem_can); +} +void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_RxFifo1FullCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_SleepCallback(CAN_HandleTypeDef *hcan) {} +void HAL_CAN_WakeUpFromRxMsgCallback(CAN_HandleTypeDef *hcan) {} void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan) { - //__asm volatile ("bkpt"); - CAN_context *ctx = get_can_ctx(hcan); - if (!ctx) return; - volatile uint32_t original_error = hcan->ErrorCode; - (void) original_error; - - // handle transmit errors in all three mailboxes - if (hcan->ErrorCode & HAL_CAN_ERROR_TX_ALST0) { - SET_BIT(hcan->Instance->sTxMailBox[0].TIR, CAN_TI0R_TXRQ); - hcan->ErrorCode &= ~HAL_CAN_ERROR_TX_ALST0; - } else if (hcan->ErrorCode & HAL_CAN_ERROR_TX_TERR0) { - tx_error(ctx, 0); - hcan->ErrorCode &= ~HAL_CAN_ERROR_EWG; - hcan->ErrorCode &= ~HAL_CAN_ERROR_ACK; - hcan->ErrorCode &= ~HAL_CAN_ERROR_TX_TERR0; - } - - if (hcan->ErrorCode & HAL_CAN_ERROR_TX_ALST1) { - SET_BIT(hcan->Instance->sTxMailBox[1].TIR, CAN_TI1R_TXRQ); - hcan->ErrorCode &= ~HAL_CAN_ERROR_TX_ALST1; - } else if (hcan->ErrorCode & HAL_CAN_ERROR_TX_TERR1) { - tx_error(ctx, 1); - hcan->ErrorCode &= ~HAL_CAN_ERROR_EWG; - hcan->ErrorCode &= ~HAL_CAN_ERROR_ACK; - hcan->ErrorCode &= ~HAL_CAN_ERROR_TX_TERR1; - } - - if (hcan->ErrorCode & HAL_CAN_ERROR_TX_ALST2) { - SET_BIT(hcan->Instance->sTxMailBox[2].TIR, CAN_TI2R_TXRQ); - hcan->ErrorCode &= ~HAL_CAN_ERROR_TX_ALST2; - } else if (hcan->ErrorCode & HAL_CAN_ERROR_TX_TERR2) { - tx_error(ctx, 2); - hcan->ErrorCode &= ~HAL_CAN_ERROR_EWG; - hcan->ErrorCode &= ~HAL_CAN_ERROR_ACK; - hcan->ErrorCode &= ~HAL_CAN_ERROR_TX_TERR2; - } - - if (hcan->ErrorCode) - ctx->unexpected_errors++; + HAL_CAN_ResetError(hcan); } - diff --git a/Firmware/communication/interface_can.hpp b/Firmware/communication/interface_can.hpp index 2cd487b0..19855047 100644 --- a/Firmware/communication/interface_can.hpp +++ b/Firmware/communication/interface_can.hpp @@ -1,55 +1,57 @@ #ifndef __INTERFACE_CAN_HPP #define __INTERFACE_CAN_HPP -#include "fibre/protocol.hpp" -#include #include +#include +#include "fibre/protocol.hpp" +#include "odrive_main.h" +#include "can_helpers.hpp" -struct CAN_context { - CAN_HandleTypeDef *handle = nullptr; - uint8_t node_id = 0; - uint64_t serial_number = 0; +#define CAN_CLK_HZ (42000000) +#define CAN_CLK_MHZ (42) - uint32_t node_ids_in_use_0[4]; // 128 bits (indicate if a node ID was in use up to 1 second ago) - uint32_t node_ids_in_use_1[4]; // 128 bits (indicats if a node ID was in use 1-2 seconds ago) - - uint32_t last_heartbeat_mailbox = 0; - uint32_t tx_msg_cnt = 0; - uint32_t node_id_expiry = 0; - - uint8_t node_id_rng_state = 0; - - osSemaphoreId sem_send_heartbeat; - - // count occurrence various callbacks - uint32_t TxMailboxCompleteCallbackCnt = 0; - uint32_t TxMailboxAbortCallbackCnt = 0; - int RxFifo0MsgPendingCallbackCnt = 0; - int RxFifo0FullCallbackCnt = 0; - int RxFifo1MsgPendingCallbackCnt = 0; - int RxFifo1FullCallbackCnt = 0; - int SleepCallbackCnt = 0; - int WakeUpFromRxMsgCallbackCnt = 0; - int ErrorCallbackCnt = 0; - - uint32_t received_msg_cnt = 0; - uint32_t received_ack = 0; - uint32_t unexpected_errors = 0; - uint32_t unhandled_messages = 0; - - auto make_protocol_definitions() { - return make_protocol_member_list( - make_protocol_ro_property("node_id", &node_id), - make_protocol_ro_property("TxMailboxCompleteCallbackCnt", &TxMailboxCompleteCallbackCnt), - make_protocol_ro_property("TxMailboxAbortCallbackCnt", &TxMailboxAbortCallbackCnt), - make_protocol_ro_property("received_msg_cnt", &received_msg_cnt), - make_protocol_ro_property("received_ack", &received_ack), - make_protocol_ro_property("unexpected_errors", &unexpected_errors), - make_protocol_ro_property("unhandled_messages", &unhandled_messages) - ); - } +// Anonymous enum for defining the most common CAN baud rates +enum { + CAN_BAUD_125K = 125000, + CAN_BAUD_250K = 250000, + CAN_BAUD_500K = 500000, + CAN_BAUD_1000K = 1000000, + CAN_BAUD_1M = 1000000 }; -bool start_can_server(CAN_context& ctx, CAN_TypeDef *hcan, uint64_t serial_number); +class ODriveCAN : public ODriveIntf::CanIntf { + public: + struct Config_t { + uint32_t baud_rate = CAN_BAUD_250K; + Protocol protocol = PROTOCOL_SIMPLE; + }; -#endif // __INTERFACE_CAN_HPP + ODriveCAN(ODriveCAN::Config_t &config, CAN_HandleTypeDef *handle); + + // Thread Relevant Data + osThreadId thread_id_; + const uint32_t stack_size_ = 1024; // Bytes + Error error_ = ERROR_NONE; + + volatile bool thread_id_valid_ = false; + bool start_can_server(); + void can_server_thread(); + void send_heartbeat(Axis *axis); + void reinit_can(); + + void set_error(Error error); + + // I/O Functions + uint32_t available(); + uint32_t write(can_Message_t &txmsg); + bool read(can_Message_t &rxmsg); + + ODriveCAN::Config_t &config_; + +private: + CAN_HandleTypeDef *handle_ = nullptr; + + void set_baud_rate(uint32_t baudRate); +}; + +#endif // __INTERFACE_CAN_HPP diff --git a/Firmware/communication/interface_i2c.cpp b/Firmware/communication/interface_i2c.cpp index 4eb0fdd2..d94ed58f 100644 --- a/Firmware/communication/interface_i2c.cpp +++ b/Firmware/communication/interface_i2c.cpp @@ -8,7 +8,7 @@ #define I2C_RX_BUFFER_PREAMBLE_SIZE 4 #define I2C_TX_BUFFER_SIZE 128 -I2CStats_t i2c_stats_ = {0}; +I2CStats_t i2c_stats_; static uint8_t i2c_rx_buffer[I2C_RX_BUFFER_PREAMBLE_SIZE + I2C_RX_BUFFER_SIZE]; static uint8_t i2c_tx_buffer[I2C_TX_BUFFER_SIZE]; diff --git a/Firmware/communication/interface_uart.cpp b/Firmware/communication/interface_uart.cpp index b3141138..aa195574 100644 --- a/Firmware/communication/interface_uart.cpp +++ b/Firmware/communication/interface_uart.cpp @@ -3,7 +3,7 @@ #include "ascii_protocol.hpp" -#include +#include #include #include @@ -22,6 +22,7 @@ static uint32_t dma_last_rcv_idx; // static thread_local uint32_t deadline_ms = 0; osThreadId uart_thread; +const uint32_t stack_size_uart_thread = 4096; // Bytes class UART4Sender : public StreamSink { @@ -61,13 +62,19 @@ static void uart_server_thread(void * ctx) { (void) ctx; for (;;) { + osDelay(1); + // Check for UART errors and restart recieve DMA transfer if required - if (huart4.ErrorCode != HAL_UART_ERROR_NONE) { + if (huart4.RxState != HAL_UART_STATE_BUSY_RX) { HAL_UART_AbortReceive(&huart4); HAL_UART_Receive_DMA(&huart4, dma_rx_buffer, sizeof(dma_rx_buffer)); + dma_last_rcv_idx = 0; } // Fetch the circular buffer "write pointer", where it would write next uint32_t new_rcv_idx = UART_RX_BUFFER_SIZE - huart4.hdmarx->Instance->NDTR; + if (new_rcv_idx > UART_RX_BUFFER_SIZE) { // defensive programming + continue; + } // deadline_ms = timeout_to_deadline(PROTOCOL_SERVER_TIMEOUT_MS); // Process bytes in one or two chunks (two in case there was a wrap) @@ -85,8 +92,6 @@ static void uart_server_thread(void * ctx) { new_rcv_idx - dma_last_rcv_idx, uart4_stream_output); dma_last_rcv_idx = new_rcv_idx; } - - osDelay(1); }; } @@ -95,10 +100,10 @@ void start_uart_server() { // We dont use interrupts to fetch the data, instead we periodically read // data out of the circular buffer into a parse buffer, controlled by a state machine HAL_UART_Receive_DMA(&huart4, dma_rx_buffer, sizeof(dma_rx_buffer)); - dma_last_rcv_idx = UART_RX_BUFFER_SIZE - huart4.hdmarx->Instance->NDTR; + dma_last_rcv_idx = 0; // Start UART communication thread - osThreadDef(uart_server_thread_def, uart_server_thread, osPriorityNormal, 0, 1024 /* the ascii protocol needs considerable stack space */); + osThreadDef(uart_server_thread_def, uart_server_thread, osPriorityNormal, 0, stack_size_uart_thread / sizeof(StackType_t) /* the ascii protocol needs considerable stack space */); uart_thread = osThreadCreate(osThread(uart_server_thread_def), NULL); } diff --git a/Firmware/communication/interface_uart.h b/Firmware/communication/interface_uart.h index 8ef39ec4..65033a6f 100644 --- a/Firmware/communication/interface_uart.h +++ b/Firmware/communication/interface_uart.h @@ -11,6 +11,7 @@ extern "C" { #include extern osThreadId uart_thread; +extern const uint32_t stack_size_uart_thread; void start_uart_server(void); diff --git a/Firmware/communication/interface_usb.cpp b/Firmware/communication/interface_usb.cpp index 036a8203..bbdabb3d 100644 --- a/Firmware/communication/interface_usb.cpp +++ b/Firmware/communication/interface_usb.cpp @@ -2,7 +2,7 @@ #include "interface_usb.h" #include "ascii_protocol.hpp" -#include +#include #include #include @@ -14,7 +14,8 @@ #include osThreadId usb_thread; -USBStats_t usb_stats_ = {0}; +const uint32_t stack_size_usb_thread = 4096; // Bytes +USBStats_t usb_stats_; class USBSender : public PacketSink { public: @@ -60,7 +61,7 @@ public: // Loop to ensure all bytes get sent while (length) { size_t chunk = length < USB_TX_DATA_SIZE ? length : USB_TX_DATA_SIZE; - if (output_.process_packet(buffer, length) != 0) + if (output_.process_packet(buffer, chunk) != 0) return -1; buffer += chunk; length -= chunk; @@ -126,7 +127,7 @@ static void usb_server_thread(void * ctx) { // CDC Interface if (CDC_interface.data_pending) { CDC_interface.data_pending = false; - if (board_config.enable_ascii_protocol_on_usb) { + if (odrv.config_.enable_ascii_protocol_on_usb) { ASCII_protocol_parse_stream(CDC_interface.rx_buf, CDC_interface.rx_len, usb_stream_output); } else { @@ -177,6 +178,6 @@ void usb_rx_process_packet(uint8_t *buf, uint32_t len, uint8_t endpoint_pair) { void start_usb_server() { // Start USB communication thread - osThreadDef(usb_server_thread_def, usb_server_thread, osPriorityNormal, 0, 1024); + osThreadDef(usb_server_thread_def, usb_server_thread, osPriorityNormal, 0, stack_size_usb_thread / sizeof(StackType_t)); usb_thread = osThreadCreate(osThread(usb_server_thread_def), NULL); } diff --git a/Firmware/communication/interface_usb.h b/Firmware/communication/interface_usb.h index 0a5b94ff..c78ecc5d 100644 --- a/Firmware/communication/interface_usb.h +++ b/Firmware/communication/interface_usb.h @@ -12,6 +12,7 @@ extern "C" { #include extern osThreadId usb_thread; +extern const uint32_t stack_size_usb_thread; typedef struct { uint32_t rx_cnt; diff --git a/Firmware/doctest/doctest.h b/Firmware/doctest/doctest.h new file mode 100644 index 00000000..4f3a0e33 --- /dev/null +++ b/Firmware/doctest/doctest.h @@ -0,0 +1,5956 @@ +// ====================================================================== lgtm [cpp/missing-header-guard] +// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED BY CMAKE! == +// ====================================================================== +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2019 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying file LICENSE.txt or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/onqtam/doctest/blob/master/doc/markdown/readme.md +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= +// +// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt +// +// The concept of subcases (sections in Catch) and expression decomposition are from there. +// Some parts of the code are taken directly: +// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> +// - the Approx() helper class for floating point comparison +// - colors in the console +// - breaking into a debugger +// - signal / SEH handling +// - timer +// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) +// +// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= + +#ifndef DOCTEST_LIBRARY_INCLUDED +#define DOCTEST_LIBRARY_INCLUDED + +// ================================================================================================= +// == VERSION ====================================================================================== +// ================================================================================================= + +#define DOCTEST_VERSION_MAJOR 2 +#define DOCTEST_VERSION_MINOR 3 +#define DOCTEST_VERSION_PATCH 7 +#define DOCTEST_VERSION_STR "2.3.7" + +#define DOCTEST_VERSION \ + (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) + +// ================================================================================================= +// == COMPILER VERSION ============================================================================= +// ================================================================================================= + +// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect + +#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR)*10000000 + (MINOR)*100000 + (PATCH)) + +// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... +#if defined(_MSC_VER) && defined(_MSC_FULL_VER) +#if _MSC_VER == _MSC_FULL_VER / 10000 +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) +#else // MSVC +#define DOCTEST_MSVC \ + DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) +#endif // MSVC +#endif // MSVC +#if defined(__clang__) && defined(__clang_minor__) +#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && \ + !defined(__INTEL_COMPILER) +#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#endif // GCC + +#ifndef DOCTEST_MSVC +#define DOCTEST_MSVC 0 +#endif // DOCTEST_MSVC +#ifndef DOCTEST_CLANG +#define DOCTEST_CLANG 0 +#endif // DOCTEST_CLANG +#ifndef DOCTEST_GCC +#define DOCTEST_GCC 0 +#endif // DOCTEST_GCC + +// ================================================================================================= +// == COMPILER WARNINGS HELPERS ==================================================================== +// ================================================================================================= + +#if DOCTEST_CLANG +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) +#else // DOCTEST_CLANG +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_CLANG + +#if DOCTEST_GCC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") +#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) +#else // DOCTEST_GCC +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH +#define DOCTEST_GCC_SUPPRESS_WARNING(w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_GCC + +#if DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) +#else // DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_MSVC + +// ================================================================================================= +// == COMPILER WARNINGS ============================================================================ +// ================================================================================================= + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-local-typedef") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") +DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-local-typedefs") +DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4616) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4619) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4996) // The compiler encountered a deprecated declaration +DOCTEST_MSVC_SUPPRESS_WARNING(4706) // assignment within conditional expression +DOCTEST_MSVC_SUPPRESS_WARNING(4512) // 'class' : assignment operator could not be generated +DOCTEST_MSVC_SUPPRESS_WARNING(4127) // conditional expression is constant +DOCTEST_MSVC_SUPPRESS_WARNING(4820) // padding +DOCTEST_MSVC_SUPPRESS_WARNING(4625) // copy constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4626) // assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5027) // move assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5026) // move constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4640) // construction of local static object is not thread-safe +// static analysis +DOCTEST_MSVC_SUPPRESS_WARNING(26439) // This kind of function may not throw. Declare it 'noexcept' +DOCTEST_MSVC_SUPPRESS_WARNING(26495) // Always initialize a member variable +DOCTEST_MSVC_SUPPRESS_WARNING(26451) // Arithmetic overflow ... +DOCTEST_MSVC_SUPPRESS_WARNING(26444) // Avoid unnamed objects with custom construction and dtr... +DOCTEST_MSVC_SUPPRESS_WARNING(26812) // Prefer 'enum class' over 'enum' + +// 4548 - expression before comma has no effect; expected expression with side - effect +// 4265 - class has virtual functions, but destructor is not virtual +// 4986 - exception specification does not match previous declaration +// 4350 - behavior change: 'member1' called instead of 'member2' +// 4668 - 'x' is not defined as a preprocessor macro, replacing with '0' for '#if/#elif' +// 4365 - conversion from 'int' to 'unsigned long', signed/unsigned mismatch +// 4774 - format string expected in argument 'x' is not a string literal +// 4820 - padding in structs + +// only 4 should be disabled globally: +// - 4514 # unreferenced inline function has been removed +// - 4571 # SEH related +// - 4710 # function not inlined +// - 4711 # function 'x' selected for automatic inline expansion + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + DOCTEST_MSVC_SUPPRESS_WARNING(4548) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4265) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4986) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4350) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4668) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5105) + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP + +// ================================================================================================= +// == FEATURE DETECTION ============================================================================ +// ================================================================================================= + +// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support +// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx +// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html +// MSVC version table: +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) +// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) +// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) +// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) +// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) +// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) +// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) +// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) + +#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#define DOCTEST_CONFIG_WINDOWS_SEH +#endif // MSVC +#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) +#undef DOCTEST_CONFIG_WINDOWS_SEH +#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH + +#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && \ + !defined(__EMSCRIPTEN__) +#define DOCTEST_CONFIG_POSIX_SIGNALS +#endif // _WIN32 +#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) +#undef DOCTEST_CONFIG_POSIX_SIGNALS +#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // no exceptions +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) +#define DOCTEST_CONFIG_IMPLEMENT +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#if defined(_WIN32) || defined(__CYGWIN__) +#if DOCTEST_MSVC +#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) +#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) +#else // MSVC +#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) +#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) +#endif // MSVC +#else // _WIN32 +#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define DOCTEST_SYMBOL_IMPORT +#endif // _WIN32 + +#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#ifdef DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT +#else // DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT +#endif // DOCTEST_CONFIG_IMPLEMENT +#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#define DOCTEST_INTERFACE +#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL + +#define DOCTEST_EMPTY + +#if DOCTEST_MSVC +#define DOCTEST_NOINLINE __declspec(noinline) +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#else // MSVC +#define DOCTEST_NOINLINE __attribute__((noinline)) +#define DOCTEST_UNUSED __attribute__((unused)) +#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) +#endif // MSVC + +// ================================================================================================= +// == FEATURE DETECTION END ======================================================================== +// ================================================================================================= + +// internal macros for string concatenation and anonymous variable name generation +#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 +#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) +#ifdef __COUNTER__ // not standard and may be missing for some compilers +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __COUNTER__) +#else // __COUNTER__ +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __LINE__) +#endif // __COUNTER__ + +#define DOCTEST_TOSTR(x) #x + +#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x& +#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x +#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE + +// not using __APPLE__ because... this is how Catch does it +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED +#define DOCTEST_PLATFORM_MAC +#elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED) +#define DOCTEST_PLATFORM_IPHONE +#elif defined(_WIN32) +#define DOCTEST_PLATFORM_WINDOWS +#else // DOCTEST_PLATFORM +#define DOCTEST_PLATFORM_LINUX +#endif // DOCTEST_PLATFORM + +#define DOCTEST_GLOBAL_NO_WARNINGS(var) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-variable") \ + static int var DOCTEST_UNUSED // NOLINT(fuchsia-statically-constructed-objects,cert-err58-cpp) +#define DOCTEST_GLOBAL_NO_WARNINGS_END() DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_BREAK_INTO_DEBUGGER +// should probably take a look at https://github.com/scottt/debugbreak +#ifdef DOCTEST_PLATFORM_MAC +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) +#elif DOCTEST_MSVC +#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() +#elif defined(__MINGW32__) +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") +extern "C" __declspec(dllimport) void __stdcall DebugBreak(); +DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() +#else // linux +#define DOCTEST_BREAK_INTO_DEBUGGER() ((void)0) +#endif // linux +#endif // DOCTEST_BREAK_INTO_DEBUGGER + +// this is kept here for backwards compatibility since the config option was changed +#ifdef DOCTEST_CONFIG_USE_IOSFWD +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif // DOCTEST_CONFIG_USE_IOSFWD + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#include +#include +#include +#else // DOCTEST_CONFIG_USE_STD_HEADERS + +#if DOCTEST_CLANG +// to detect if libc++ is being used with clang (the _LIBCPP_VERSION identifier) +#include +#endif // clang + +#ifdef _LIBCPP_VERSION +#define DOCTEST_STD_NAMESPACE_BEGIN _LIBCPP_BEGIN_NAMESPACE_STD +#define DOCTEST_STD_NAMESPACE_END _LIBCPP_END_NAMESPACE_STD +#else // _LIBCPP_VERSION +#define DOCTEST_STD_NAMESPACE_BEGIN namespace std { +#define DOCTEST_STD_NAMESPACE_END } +#endif // _LIBCPP_VERSION + +// Forward declaring 'X' in namespace std is not permitted by the C++ Standard. +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643) + +DOCTEST_STD_NAMESPACE_BEGIN // NOLINT (cert-dcl58-cpp) +typedef decltype(nullptr) nullptr_t; +template +struct char_traits; +template <> +struct char_traits; +template +class basic_ostream; +typedef basic_ostream> ostream; +template +class tuple; +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/onqtam/doctest/issues/183 +template +class allocator; +template +class basic_string; +using string = basic_string, allocator>; +#endif // VS 2019 +DOCTEST_STD_NAMESPACE_END + +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#include +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + +namespace doctest { + +DOCTEST_INTERFACE extern bool is_running_in_test; + +// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings with length +// of up to 23 chars on the stack before going on the heap - the last byte of the buffer is used for: +// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) +// - if small - capacity left before going on the heap - using the lowest 5 bits +// - if small - 2 bits are left unused - the second and third highest ones +// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) +// and the "is small" bit remains "0" ("as well as the capacity left") so its OK +// Idea taken from this lecture about the string implementation of facebook/folly - fbstring +// https://www.youtube.com/watch?v=kPR8h4-qZdk +// TODO: +// - optimizations - like not deleting memory unnecessarily in operator= and etc. +// - resize/reserve/clear +// - substr +// - replace +// - back/front +// - iterator stuff +// - find & friends +// - push_back/pop_back +// - assign/insert/erase +// - relational operators as free functions - taking const char* as one of the params +class DOCTEST_INTERFACE String +{ + static const unsigned len = 24; //!OCLINT avoid private static members + static const unsigned last = len - 1; //!OCLINT avoid private static members + + struct view // len should be more than sizeof(view) - because of the final byte for flags + { + char* ptr; + unsigned size; + unsigned capacity; + }; + + union + { + char buf[len]; + view data; + }; + + bool isOnStack() const { return (buf[last] & 128) == 0; } + void setOnHeap(); + void setLast(unsigned in = last); + + void copy(const String& other); + +public: + String(); + ~String(); + + // cppcheck-suppress noExplicitConstructor + String(const char* in); + String(const char* in, unsigned in_size); + + String(const String& other); + String& operator=(const String& other); + + String& operator+=(const String& other); + String operator+(const String& other) const; + + String(String&& other); + String& operator=(String&& other); + + char operator[](unsigned i) const; + char& operator[](unsigned i); + + // the only functions I'm willing to leave in the interface - available for inlining + const char* c_str() const { return const_cast(this)->c_str(); } // NOLINT + char* c_str() { + if(isOnStack()) + return reinterpret_cast(buf); + return data.ptr; + } + + unsigned size() const; + unsigned capacity() const; + + int compare(const char* other, bool no_case = false) const; + int compare(const String& other, bool no_case = false) const; +}; + +DOCTEST_INTERFACE bool operator==(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator!=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs); + +DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in); + +namespace Color { + enum Enum + { + None = 0, + White, + Red, + Green, + Blue, + Cyan, + Yellow, + Grey, + + Bright = 0x10, + + BrightRed = Bright | Red, + BrightGreen = Bright | Green, + LightGrey = Bright | Grey, + BrightWhite = Bright | White + }; + + DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, Color::Enum code); +} // namespace Color + +namespace assertType { + enum Enum + { + // macro traits + + is_warn = 1, + is_check = 2 * is_warn, + is_require = 2 * is_check, + + is_normal = 2 * is_require, + is_throws = 2 * is_normal, + is_throws_as = 2 * is_throws, + is_throws_with = 2 * is_throws_as, + is_nothrow = 2 * is_throws_with, + + is_false = 2 * is_nothrow, + is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types + + is_eq = 2 * is_unary, + is_ne = 2 * is_eq, + + is_lt = 2 * is_ne, + is_gt = 2 * is_lt, + + is_ge = 2 * is_gt, + is_le = 2 * is_ge, + + // macro types + + DT_WARN = is_normal | is_warn, + DT_CHECK = is_normal | is_check, + DT_REQUIRE = is_normal | is_require, + + DT_WARN_FALSE = is_normal | is_false | is_warn, + DT_CHECK_FALSE = is_normal | is_false | is_check, + DT_REQUIRE_FALSE = is_normal | is_false | is_require, + + DT_WARN_THROWS = is_throws | is_warn, + DT_CHECK_THROWS = is_throws | is_check, + DT_REQUIRE_THROWS = is_throws | is_require, + + DT_WARN_THROWS_AS = is_throws_as | is_warn, + DT_CHECK_THROWS_AS = is_throws_as | is_check, + DT_REQUIRE_THROWS_AS = is_throws_as | is_require, + + DT_WARN_THROWS_WITH = is_throws_with | is_warn, + DT_CHECK_THROWS_WITH = is_throws_with | is_check, + DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, + + DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, + DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, + DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, + + DT_WARN_NOTHROW = is_nothrow | is_warn, + DT_CHECK_NOTHROW = is_nothrow | is_check, + DT_REQUIRE_NOTHROW = is_nothrow | is_require, + + DT_WARN_EQ = is_normal | is_eq | is_warn, + DT_CHECK_EQ = is_normal | is_eq | is_check, + DT_REQUIRE_EQ = is_normal | is_eq | is_require, + + DT_WARN_NE = is_normal | is_ne | is_warn, + DT_CHECK_NE = is_normal | is_ne | is_check, + DT_REQUIRE_NE = is_normal | is_ne | is_require, + + DT_WARN_GT = is_normal | is_gt | is_warn, + DT_CHECK_GT = is_normal | is_gt | is_check, + DT_REQUIRE_GT = is_normal | is_gt | is_require, + + DT_WARN_LT = is_normal | is_lt | is_warn, + DT_CHECK_LT = is_normal | is_lt | is_check, + DT_REQUIRE_LT = is_normal | is_lt | is_require, + + DT_WARN_GE = is_normal | is_ge | is_warn, + DT_CHECK_GE = is_normal | is_ge | is_check, + DT_REQUIRE_GE = is_normal | is_ge | is_require, + + DT_WARN_LE = is_normal | is_le | is_warn, + DT_CHECK_LE = is_normal | is_le | is_check, + DT_REQUIRE_LE = is_normal | is_le | is_require, + + DT_WARN_UNARY = is_normal | is_unary | is_warn, + DT_CHECK_UNARY = is_normal | is_unary | is_check, + DT_REQUIRE_UNARY = is_normal | is_unary | is_require, + + DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, + DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, + DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, + }; +} // namespace assertType + +DOCTEST_INTERFACE const char* assertString(assertType::Enum at); +DOCTEST_INTERFACE const char* failureString(assertType::Enum at); +DOCTEST_INTERFACE const char* skipPathFromFilename(const char* file); + +struct DOCTEST_INTERFACE TestCaseData +{ + const char* m_file; // the file in which the test was registered + unsigned m_line; // the line where the test was registered + const char* m_name; // name of the test case + const char* m_test_suite; // the test suite in which the test was added + const char* m_description; + bool m_skip; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; +}; + +struct DOCTEST_INTERFACE AssertData +{ + // common - for all asserts + const TestCaseData* m_test_case; + assertType::Enum m_at; + const char* m_file; + int m_line; + const char* m_expr; + bool m_failed; + + // exception-related - for all asserts + bool m_threw; + String m_exception; + + // for normal asserts + String m_decomp; + + // for specific exception-related asserts + bool m_threw_as; + const char* m_exception_type; + const char* m_exception_string; +}; + +struct DOCTEST_INTERFACE MessageData +{ + String m_string; + const char* m_file; + int m_line; + assertType::Enum m_severity; +}; + +struct DOCTEST_INTERFACE SubcaseSignature +{ + String m_name; + const char* m_file; + int m_line; + + bool operator<(const SubcaseSignature& other) const; +}; + +struct DOCTEST_INTERFACE IContextScope +{ + IContextScope(); + virtual ~IContextScope(); + virtual void stringify(std::ostream*) const = 0; +}; + +struct ContextOptions //!OCLINT too many fields +{ + std::ostream* cout; // stdout stream - std::cout by default + std::ostream* cerr; // stderr stream - std::cerr by default + String binary_name; // the test binary name + + // == parameters from the command line + String out; // output filename + String order_by; // how tests should be ordered + unsigned rand_seed; // the seed for rand ordering + + unsigned first; // the first (matching) test to be executed + unsigned last; // the last (matching) test to be executed + + int abort_after; // stop tests after this many failed assertions + int subcase_filter_levels; // apply the subcase filters for the first N levels + + bool success; // include successful assertions in output + bool case_sensitive; // if filtering should be case sensitive + bool exit; // if the program should be exited after the tests are ran/whatever + bool duration; // print the time duration of each test case + bool no_throw; // to skip exceptions-related assertion macros + bool no_exitcode; // if the framework should return 0 as the exitcode + bool no_run; // to not run the tests at all (can be done with an "*" exclude) + bool no_version; // to not print the version of the framework + bool no_colors; // if output to the console should be colorized + bool force_colors; // forces the use of colors even when a tty cannot be detected + bool no_breaks; // to not break into the debugger + bool no_skip; // don't skip test cases which are marked to be skipped + bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): + bool no_path_in_filenames; // if the path to files should be removed from the output + bool no_line_numbers; // if source code line numbers should be omitted from the output + bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! + + bool help; // to print the help + bool version; // to print the version + bool count; // if only the count of matching tests is to be retrieved + bool list_test_cases; // to list all tests matching the filters + bool list_test_suites; // to list all suites matching the filters + bool list_reporters; // lists all registered reporters +}; + +namespace detail { +#if defined(DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING) || defined(DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS) + template + struct enable_if + {}; + + template + struct enable_if + { typedef TYPE type; }; +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING) || DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + template struct remove_reference { typedef T type; }; + template struct remove_reference { typedef T type; }; + template struct remove_reference { typedef T type; }; + + template struct remove_const { typedef T type; }; + template struct remove_const { typedef T type; }; + // clang-format on + + template + struct deferred_false + // cppcheck-suppress unusedStructMember + { static const bool value = false; }; + + namespace has_insertion_operator_impl { + typedef char no; + typedef char yes[2]; + + struct any_t + { + template + // cppcheck-suppress noExplicitConstructor + any_t(const DOCTEST_REF_WRAP(T)); + }; + + yes& testStreamable(std::ostream&); + no testStreamable(no); + + no operator<<(const std::ostream&, const any_t&); + + template + struct has_insertion_operator + { + static std::ostream& s; + static const DOCTEST_REF_WRAP(T) t; + static const bool value = sizeof(decltype(testStreamable(s << t))) == sizeof(yes); + }; + } // namespace has_insertion_operator_impl + + template + struct has_insertion_operator : has_insertion_operator_impl::has_insertion_operator + {}; + + DOCTEST_INTERFACE void my_memcpy(void* dest, const void* src, unsigned num); + + DOCTEST_INTERFACE std::ostream* getTlsOss(); // returns a thread-local ostringstream + DOCTEST_INTERFACE String getTlsOssResult(); + + template + struct StringMakerBase + { + template + static String convert(const DOCTEST_REF_WRAP(T)) { + return "{?}"; + } + }; + + template <> + struct StringMakerBase + { + template + static String convert(const DOCTEST_REF_WRAP(T) in) { + *getTlsOss() << in; + return getTlsOssResult(); + } + }; + + DOCTEST_INTERFACE String rawMemoryToString(const void* object, unsigned size); + + template + String rawMemoryToString(const DOCTEST_REF_WRAP(T) object) { + return rawMemoryToString(&object, sizeof(object)); + } + + template + const char* type_to_string() { + return "<>"; + } +} // namespace detail + +template +struct StringMaker : public detail::StringMakerBase::value> +{}; + +template +struct StringMaker +{ + template + static String convert(U* p) { + if(p) + return detail::rawMemoryToString(p); + return "NULL"; + } +}; + +template +struct StringMaker +{ + static String convert(R C::*p) { + if(p) + return detail::rawMemoryToString(p); + return "NULL"; + } +}; + +template +String toString(const DOCTEST_REF_WRAP(T) value) { + return StringMaker::convert(value); +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(char* in); +DOCTEST_INTERFACE String toString(const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(bool in); +DOCTEST_INTERFACE String toString(float in); +DOCTEST_INTERFACE String toString(double in); +DOCTEST_INTERFACE String toString(double long in); + +DOCTEST_INTERFACE String toString(char in); +DOCTEST_INTERFACE String toString(char signed in); +DOCTEST_INTERFACE String toString(char unsigned in); +DOCTEST_INTERFACE String toString(int short in); +DOCTEST_INTERFACE String toString(int short unsigned in); +DOCTEST_INTERFACE String toString(int in); +DOCTEST_INTERFACE String toString(int unsigned in); +DOCTEST_INTERFACE String toString(int long in); +DOCTEST_INTERFACE String toString(int long unsigned in); +DOCTEST_INTERFACE String toString(int long long in); +DOCTEST_INTERFACE String toString(int long long unsigned in); +DOCTEST_INTERFACE String toString(std::nullptr_t in); + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/onqtam/doctest/issues/183 +DOCTEST_INTERFACE String toString(const std::string& in); +#endif // VS 2019 + +class DOCTEST_INTERFACE Approx +{ +public: + explicit Approx(double value); + + Approx operator()(double value) const; + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + explicit Approx(const T& value, + typename detail::enable_if::value>::type* = + static_cast(nullptr)) { + *this = Approx(static_cast(value)); + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& epsilon(double newEpsilon); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename detail::enable_if::value, Approx&>::type epsilon( + const T& newEpsilon) { + m_epsilon = static_cast(newEpsilon); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& scale(double newScale); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename detail::enable_if::value, Approx&>::type scale( + const T& newScale) { + m_scale = static_cast(newScale); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator==(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator!=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator<=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator>=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator< (const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator> (const Approx & lhs, double rhs); + + DOCTEST_INTERFACE friend String toString(const Approx& in); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_APPROX_PREFIX \ + template friend typename detail::enable_if::value, bool>::type + + DOCTEST_APPROX_PREFIX operator==(const T& lhs, const Approx& rhs) { return operator==(double(lhs), rhs); } + DOCTEST_APPROX_PREFIX operator==(const Approx& lhs, const T& rhs) { return operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator!=(const T& lhs, const Approx& rhs) { return !operator==(lhs, rhs); } + DOCTEST_APPROX_PREFIX operator!=(const Approx& lhs, const T& rhs) { return !operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator<=(const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator<=(const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator< (const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator< (const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) && lhs != rhs; } +#undef DOCTEST_APPROX_PREFIX +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format on + +private: + double m_epsilon; + double m_scale; + double m_value; +}; + +DOCTEST_INTERFACE String toString(const Approx& in); + +DOCTEST_INTERFACE const ContextOptions* getContextOptions(); + +#if !defined(DOCTEST_CONFIG_DISABLE) + +namespace detail { + // clang-format off +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + template struct decay_array { typedef T type; }; + template struct decay_array { typedef T* type; }; + template struct decay_array { typedef T* type; }; + + template struct not_char_pointer { enum { value = 1 }; }; + template<> struct not_char_pointer { enum { value = 0 }; }; + template<> struct not_char_pointer { enum { value = 0 }; }; + + template struct can_use_op : public not_char_pointer::type> {}; +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + + struct DOCTEST_INTERFACE TestFailureException + { + }; + + DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + [[noreturn]] +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_INTERFACE void throwException(); + + struct DOCTEST_INTERFACE Subcase + { + SubcaseSignature m_signature; + bool m_entered = false; + + Subcase(const String& name, const char* file, int line); + ~Subcase(); + + operator bool() const; + }; + + template + String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char* op, + const DOCTEST_REF_WRAP(R) rhs) { + return toString(lhs) + op + toString(rhs); + } + +#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ + template \ + DOCTEST_NOINLINE Result operator op(const DOCTEST_REF_WRAP(R) rhs) { \ + bool res = op_macro(lhs, rhs); \ + if(m_at & assertType::is_false) \ + res = !res; \ + if(!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } + + // more checks could be added - like in Catch: + // https://github.com/catchorg/Catch2/pull/1480/files + // https://github.com/catchorg/Catch2/pull/1481/files +#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ + template \ + rt& operator op(const R&) { \ + static_assert(deferred_false::value, \ + "Expression Too Complex Please Rewrite As Binary Comparison!"); \ + return *this; \ + } + + struct DOCTEST_INTERFACE Result + { + bool m_passed; + String m_decomp; + + Result(bool passed, const String& decomposition = String()); + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Result, &) + DOCTEST_FORBIT_EXPRESSION(Result, ^) + DOCTEST_FORBIT_EXPRESSION(Result, |) + DOCTEST_FORBIT_EXPRESSION(Result, &&) + DOCTEST_FORBIT_EXPRESSION(Result, ||) + DOCTEST_FORBIT_EXPRESSION(Result, ==) + DOCTEST_FORBIT_EXPRESSION(Result, !=) + DOCTEST_FORBIT_EXPRESSION(Result, <) + DOCTEST_FORBIT_EXPRESSION(Result, >) + DOCTEST_FORBIT_EXPRESSION(Result, <=) + DOCTEST_FORBIT_EXPRESSION(Result, >=) + DOCTEST_FORBIT_EXPRESSION(Result, =) + DOCTEST_FORBIT_EXPRESSION(Result, +=) + DOCTEST_FORBIT_EXPRESSION(Result, -=) + DOCTEST_FORBIT_EXPRESSION(Result, *=) + DOCTEST_FORBIT_EXPRESSION(Result, /=) + DOCTEST_FORBIT_EXPRESSION(Result, %=) + DOCTEST_FORBIT_EXPRESSION(Result, <<=) + DOCTEST_FORBIT_EXPRESSION(Result, >>=) + DOCTEST_FORBIT_EXPRESSION(Result, &=) + DOCTEST_FORBIT_EXPRESSION(Result, ^=) + DOCTEST_FORBIT_EXPRESSION(Result, |=) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_GCC_SUPPRESS_WARNING_PUSH + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH + // https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 + DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch + //DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in operation + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + // clang-format off +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE bool +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE typename enable_if::value || can_use_op::value, bool>::type + inline bool eq(const char* lhs, const char* rhs) { return String(lhs) == String(rhs); } + inline bool ne(const char* lhs, const char* rhs) { return String(lhs) != String(rhs); } + inline bool lt(const char* lhs, const char* rhs) { return String(lhs) < String(rhs); } + inline bool gt(const char* lhs, const char* rhs) { return String(lhs) > String(rhs); } + inline bool le(const char* lhs, const char* rhs) { return String(lhs) <= String(rhs); } + inline bool ge(const char* lhs, const char* rhs) { return String(lhs) >= String(rhs); } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, \ + const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) l == r +#define DOCTEST_CMP_NE(l, r) l != r +#define DOCTEST_CMP_GT(l, r) l > r +#define DOCTEST_CMP_LT(l, r) l < r +#define DOCTEST_CMP_GE(l, r) l >= r +#define DOCTEST_CMP_LE(l, r) l <= r +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) eq(l, r) +#define DOCTEST_CMP_NE(l, r) ne(l, r) +#define DOCTEST_CMP_GT(l, r) gt(l, r) +#define DOCTEST_CMP_LT(l, r) lt(l, r) +#define DOCTEST_CMP_GE(l, r) ge(l, r) +#define DOCTEST_CMP_LE(l, r) le(l, r) +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + + template + // cppcheck-suppress copyCtorAndEqOperator + struct Expression_lhs + { + L lhs; + assertType::Enum m_at; + + explicit Expression_lhs(L in, assertType::Enum at) + : lhs(in) + , m_at(at) {} + + DOCTEST_NOINLINE operator Result() { + bool res = !!lhs; + if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional + res = !res; + + if(!res || getContextOptions()->success) + return Result(res, toString(lhs)); + return Result(res); + } + + // clang-format off + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) //!OCLINT bitwise operator in conditional + // clang-format on + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) + // these 2 are unfortunate because they should be allowed - they have higher precedence over the comparisons, but the + // ExpressionDecomposer class uses the left shift operator to capture the left operand of the binary expression... + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + struct DOCTEST_INTERFACE ExpressionDecomposer + { + assertType::Enum m_at; + + ExpressionDecomposer(assertType::Enum at); + + // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator precedence table) + // but then there will be warnings from GCC about "-Wparentheses" and since "_Pragma()" is problematic this will stay for now... + // https://github.com/catchorg/Catch2/issues/870 + // https://github.com/catchorg/Catch2/issues/565 + template + Expression_lhs operator<<(const DOCTEST_REF_WRAP(L) operand) { + return Expression_lhs(operand, m_at); + } + }; + + struct DOCTEST_INTERFACE TestSuite + { + const char* m_test_suite; + const char* m_description; + bool m_skip; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; + + TestSuite& operator*(const char* in); + + template + TestSuite& operator*(const T& in) { + in.fill(*this); + return *this; + } + }; + + typedef void (*funcType)(); + + struct DOCTEST_INTERFACE TestCase : public TestCaseData + { + funcType m_test; // a function pointer to the test case + + const char* m_type; // for templated test cases - gets appended to the real name + int m_template_id; // an ID used to distinguish between the different versions of a templated test case + String m_full_name; // contains the name (only for templated test cases!) + the template type + + TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const char* type = "", int template_id = -1); + + TestCase(const TestCase& other); + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase& operator=(const TestCase& other); + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& operator*(const char* in); + + template + TestCase& operator*(const T& in) { + in.fill(*this); + return *this; + } + + bool operator<(const TestCase& other) const; + }; + + // forward declarations of functions used by the macros + DOCTEST_INTERFACE int regTest(const TestCase& tc); + DOCTEST_INTERFACE int setTestSuite(const TestSuite& ts); + DOCTEST_INTERFACE bool isDebuggerActive(); + + template + int instantiationHelper(const T&) { return 0; } + + namespace binaryAssertComparison { + enum Enum + { + eq = 0, + ne, + gt, + lt, + ge, + le + }; + } // namespace binaryAssertComparison + + // clang-format off + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; + +#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; + // clang-format on + + DOCTEST_BINARY_RELATIONAL_OP(0, eq) + DOCTEST_BINARY_RELATIONAL_OP(1, ne) + DOCTEST_BINARY_RELATIONAL_OP(2, gt) + DOCTEST_BINARY_RELATIONAL_OP(3, lt) + DOCTEST_BINARY_RELATIONAL_OP(4, ge) + DOCTEST_BINARY_RELATIONAL_OP(5, le) + + struct DOCTEST_INTERFACE ResultBuilder : public AssertData + { + ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type = "", const char* exception_string = ""); + + void setResult(const Result& res); + + template + DOCTEST_NOINLINE void binary_assert(const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + m_failed = !RelationalComparator()(lhs, rhs); + if(m_failed || getContextOptions()->success) + m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); + } + + template + DOCTEST_NOINLINE void unary_assert(const DOCTEST_REF_WRAP(L) val) { + m_failed = !val; + + if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional + m_failed = !m_failed; + + if(m_failed || getContextOptions()->success) + m_decomp = toString(val); + } + + void translateException(); + + bool log(); + void react() const; + }; + + namespace assertAction { + enum Enum + { + nothing = 0, + dbgbreak = 1, + shouldthrow = 2 + }; + } // namespace assertAction + + DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData& ad); + + DOCTEST_INTERFACE void decomp_assert(assertType::Enum at, const char* file, int line, + const char* expr, Result result); + +#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ + do { \ + if(!is_running_in_test) { \ + if(failed) { \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + rb.m_decomp = decomp; \ + failed_out_of_a_testing_context(rb); \ + if(isDebuggerActive() && !getContextOptions()->no_breaks) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(checkIfShouldThrow(at)) \ + throwException(); \ + } \ + return; \ + } \ + } while(false) + +#define DOCTEST_ASSERT_IN_TESTS(decomp) \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + if(rb.m_failed || getContextOptions()->success) \ + rb.m_decomp = decomp; \ + if(rb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(rb.m_failed && checkIfShouldThrow(at)) \ + throwException() + + template + DOCTEST_NOINLINE void binary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + bool failed = !RelationalComparator()(lhs, rhs); + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + } + + template + DOCTEST_NOINLINE void unary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) val) { + bool failed = !val; + + if(at & assertType::is_false) //!OCLINT bitwise operator in conditional + failed = !failed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(toString(val)); + DOCTEST_ASSERT_IN_TESTS(toString(val)); + } + + struct DOCTEST_INTERFACE IExceptionTranslator + { + IExceptionTranslator(); + virtual ~IExceptionTranslator(); + virtual bool translate(String&) const = 0; + }; + + template + class ExceptionTranslator : public IExceptionTranslator //!OCLINT destructor of virtual class + { + public: + explicit ExceptionTranslator(String (*translateFunction)(T)) + : m_translateFunction(translateFunction) {} + + bool translate(String& res) const override { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { + throw; // lgtm [cpp/rethrow-no-exception] + // cppcheck-suppress catchExceptionByValue + } catch(T ex) { // NOLINT + res = m_translateFunction(ex); //!OCLINT parameter reassignment + return true; + } catch(...) {} //!OCLINT - empty catch statement +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + ((void)res); // to silence -Wunused-parameter + return false; + } + + private: + String (*m_translateFunction)(T); + }; + + DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator* et); + + template + struct StringStreamBase + { + template + static void convert(std::ostream* s, const T& in) { + *s << toString(in); + } + + // always treat char* as a string in this context - no matter + // if DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING is defined + static void convert(std::ostream* s, const char* in) { *s << String(in); } + }; + + template <> + struct StringStreamBase + { + template + static void convert(std::ostream* s, const T& in) { + *s << in; + } + }; + + template + struct StringStream : public StringStreamBase::value> + {}; + + template + void toStream(std::ostream* s, const T& value) { + StringStream::convert(s, value); + } + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + DOCTEST_INTERFACE void toStream(std::ostream* s, char* in); + DOCTEST_INTERFACE void toStream(std::ostream* s, const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + DOCTEST_INTERFACE void toStream(std::ostream* s, bool in); + DOCTEST_INTERFACE void toStream(std::ostream* s, float in); + DOCTEST_INTERFACE void toStream(std::ostream* s, double in); + DOCTEST_INTERFACE void toStream(std::ostream* s, double long in); + + DOCTEST_INTERFACE void toStream(std::ostream* s, char in); + DOCTEST_INTERFACE void toStream(std::ostream* s, char signed in); + DOCTEST_INTERFACE void toStream(std::ostream* s, char unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int short in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int short unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long long in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long long unsigned in); + + // ContextScope base class used to allow implementing methods of ContextScope + // that don't depend on the template parameter in doctest.cpp. + class DOCTEST_INTERFACE ContextScopeBase : public IContextScope { + protected: + ContextScopeBase(); + + void destroy(); + }; + + template class ContextScope : public ContextScopeBase + { + const L &lambda_; + + public: + explicit ContextScope(const L &lambda) : lambda_(lambda) {} + + ContextScope(ContextScope &&other) : lambda_(other.lambda_) {} + + void stringify(std::ostream* s) const override { lambda_(s); } + + ~ContextScope() override { destroy(); } + }; + + struct DOCTEST_INTERFACE MessageBuilder : public MessageData + { + std::ostream* m_stream; + + MessageBuilder(const char* file, int line, assertType::Enum severity); + MessageBuilder() = delete; + ~MessageBuilder(); + + template + MessageBuilder& operator<<(const T& in) { + toStream(m_stream, in); + return *this; + } + + bool log(); + void react(); + }; + + template + ContextScope MakeContextScope(const L &lambda) { + return ContextScope(lambda); + } +} // namespace detail + +#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ + struct name \ + { \ + type data; \ + name(type in = def) \ + : data(in) {} \ + void fill(detail::TestCase& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + void fill(detail::TestSuite& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + } + +DOCTEST_DEFINE_DECORATOR(test_suite, const char*, ""); +DOCTEST_DEFINE_DECORATOR(description, const char*, ""); +DOCTEST_DEFINE_DECORATOR(skip, bool, true); +DOCTEST_DEFINE_DECORATOR(timeout, double, 0); +DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); + +template +int registerExceptionTranslator(String (*translateFunction)(T)) { + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") + static detail::ExceptionTranslator exceptionTranslator(translateFunction); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + detail::registerExceptionTranslatorImpl(&exceptionTranslator); + return 0; +} + +} // namespace doctest + +// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro +// introduces an anonymous namespace in which getCurrentTestSuite gets overridden +namespace doctest_detail_test_suite_ns { +DOCTEST_INTERFACE doctest::detail::TestSuite& getCurrentTestSuite(); +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +#else // DOCTEST_CONFIG_DISABLE +template +int registerExceptionTranslator(String (*)(T)) { + return 0; +} +#endif // DOCTEST_CONFIG_DISABLE + +namespace detail { + typedef void (*assert_handler)(const AssertData&); + struct ContextState; +} // namespace detail + +class DOCTEST_INTERFACE Context +{ + detail::ContextState* p; + + void parseArgs(int argc, const char* const* argv, bool withDefaults = false); + +public: + explicit Context(int argc = 0, const char* const* argv = nullptr); + + ~Context(); + + void applyCommandLine(int argc, const char* const* argv); + + void addFilter(const char* filter, const char* value); + void clearFilters(); + void setOption(const char* option, int value); + void setOption(const char* option, const char* value); + + bool shouldExit(); + + void setAsDefaultForAssertsOutOfTestCases(); + + void setAssertHandler(detail::assert_handler ah); + + int run(); +}; + +namespace TestCaseFailureReason { + enum Enum + { + None = 0, + AssertFailure = 1, // an assertion has failed in the test case + Exception = 2, // test case threw an exception + Crash = 4, // a crash... + TooManyFailedAsserts = 8, // the abort-after option + Timeout = 16, // see the timeout decorator + ShouldHaveFailedButDidnt = 32, // see the should_fail decorator + ShouldHaveFailedAndDid = 64, // see the should_fail decorator + DidntFailExactlyNumTimes = 128, // see the expected_failures decorator + FailedExactlyNumTimes = 256, // see the expected_failures decorator + CouldHaveFailedAndDid = 512 // see the may_fail decorator + }; +} // namespace TestCaseFailureReason + +struct DOCTEST_INTERFACE CurrentTestCaseStats +{ + int numAssertsCurrentTest; + int numAssertsFailedCurrentTest; + double seconds; + int failure_flags; // use TestCaseFailureReason::Enum +}; + +struct DOCTEST_INTERFACE TestCaseException +{ + String error_string; + bool is_crash; +}; + +struct DOCTEST_INTERFACE TestRunStats +{ + unsigned numTestCases; + unsigned numTestCasesPassingFilters; + unsigned numTestSuitesPassingFilters; + unsigned numTestCasesFailed; + int numAsserts; + int numAssertsFailed; +}; + +struct QueryData +{ + const TestRunStats* run_stats = nullptr; + const TestCaseData** data = nullptr; + unsigned num_data = 0; +}; + +struct DOCTEST_INTERFACE IReporter +{ + // The constructor has to accept "const ContextOptions&" as a single argument + // which has most of the options for the run + a pointer to the stdout stream + // Reporter(const ContextOptions& in) + + // called when a query should be reported (listing test cases, printing the version, etc.) + virtual void report_query(const QueryData&) = 0; + + // called when the whole test run starts + virtual void test_run_start() = 0; + // called when the whole test run ends (caching a pointer to the input doesn't make sense here) + virtual void test_run_end(const TestRunStats&) = 0; + + // called when a test case is started (safe to cache a pointer to the input) + virtual void test_case_start(const TestCaseData&) = 0; + // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) + virtual void test_case_reenter(const TestCaseData&) = 0; + // called when a test case has ended + virtual void test_case_end(const CurrentTestCaseStats&) = 0; + + // called when an exception is thrown from the test case (or it crashes) + virtual void test_case_exception(const TestCaseException&) = 0; + + // called whenever a subcase is entered (don't cache pointers to the input) + virtual void subcase_start(const SubcaseSignature&) = 0; + // called whenever a subcase is exited (don't cache pointers to the input) + virtual void subcase_end() = 0; + + // called for each assert (don't cache pointers to the input) + virtual void log_assert(const AssertData&) = 0; + // called for each message (don't cache pointers to the input) + virtual void log_message(const MessageData&) = 0; + + // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator + // or isn't in the execution range (between first and last) (safe to cache a pointer to the input) + virtual void test_case_skipped(const TestCaseData&) = 0; + + // doctest will not be managing the lifetimes of reporters given to it but this would still be nice to have + virtual ~IReporter(); + + // can obtain all currently active contexts and stringify them if one wishes to do so + static int get_num_active_contexts(); + static const IContextScope* const* get_active_contexts(); + + // can iterate through contexts which have been stringified automatically in their destructors when an exception has been thrown + static int get_num_stringified_contexts(); + static const String* get_stringified_contexts(); +}; + +namespace detail { + typedef IReporter* (*reporterCreatorFunc)(const ContextOptions&); + + DOCTEST_INTERFACE void registerReporterImpl(const char* name, int prio, reporterCreatorFunc c, bool isReporter); + + template + IReporter* reporterCreator(const ContextOptions& o) { + return new Reporter(o); + } +} // namespace detail + +template +int registerReporter(const char* name, int priority, bool isReporter) { + detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); + return 0; +} +} // namespace doctest + +// if registering is not disabled +#if !defined(DOCTEST_CONFIG_DISABLE) + +// common code in asserts - for convenience +#define DOCTEST_ASSERT_LOG_AND_REACT(b) \ + if(b.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + b.react() + +#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) x; +#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) \ + try { \ + x; \ + } catch(...) { _DOCTEST_RB.translateException(); } +#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(x) \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ + static_cast(x); \ + DOCTEST_GCC_SUPPRESS_WARNING_POP +#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(x) x; +#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +// registers the test by initializing a dummy var with a function +#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ + global_prefix DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \ + doctest::detail::regTest( \ + doctest::detail::TestCase( \ + f, __FILE__, __LINE__, \ + doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ + decorators); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ + namespace { \ + struct der : public base \ + { \ + void f(); \ + }; \ + static void func() { \ + der v; \ + v.f(); \ + } \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ + } \ + inline DOCTEST_NOINLINE void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ + static void f(); \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ + static void f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ + static doctest::detail::funcType proxy() { return f; } \ + DOCTEST_REGISTER_FUNCTION(inline const, proxy(), decorators) \ + static void f() + +// for registering tests +#define DOCTEST_TEST_CASE(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), decorators) + +// for registering tests in classes - requires C++17 for inline variables! +#if __cplusplus >= 201703L || (DOCTEST_MSVC >= DOCTEST_COMPILER(19, 12, 0) && _MSVC_LANG >= 201703L) +#define DOCTEST_TEST_CASE_CLASS(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_PROXY_), \ + decorators) +#else // DOCTEST_TEST_CASE_CLASS +#define DOCTEST_TEST_CASE_CLASS(...) \ + TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER +#endif // DOCTEST_TEST_CASE_CLASS + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(_DOCTEST_ANON_CLASS_), c, \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), decorators) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_IMPL(...) \ + template <> \ + inline const char* type_to_string<__VA_ARGS__>() { \ + return "<" #__VA_ARGS__ ">"; \ + } +#define DOCTEST_TYPE_TO_STRING(...) \ + namespace doctest { namespace detail { \ + DOCTEST_TYPE_TO_STRING_IMPL(__VA_ARGS__) \ + } \ + } \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ + template \ + static void func(); \ + namespace { \ + template \ + struct iter; \ + template \ + struct iter> \ + { \ + iter(const char* file, unsigned line, int index) { \ + doctest::detail::regTest(doctest::detail::TestCase(func, file, line, \ + doctest_detail_test_suite_ns::getCurrentTestSuite(), \ + doctest::detail::type_to_string(), \ + int(line) * 1000 + index) \ + * dec); \ + iter>(file, line, index + 1); \ + } \ + }; \ + template <> \ + struct iter> \ + { \ + iter(const char*, unsigned, int) {} \ + }; \ + } \ + template \ + static void func() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_)) + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_CAT(anon, DUMMY)) = \ + doctest::detail::instantiationHelper(DOCTEST_CAT(id, ITERATOR)<__VA_ARGS__>(__FILE__, __LINE__, 0));\ + DOCTEST_GLOBAL_NO_WARNINGS_END() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_), __VA_ARGS__) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ + template \ + static void anon() + +#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_), __VA_ARGS__) + +// for subcases +#define DOCTEST_SUBCASE(name) \ + if(const doctest::detail::Subcase & DOCTEST_ANONYMOUS(_DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ + doctest::detail::Subcase(name, __FILE__, __LINE__)) + +// for grouping tests in test suites by using code blocks +#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ + namespace ns_name { namespace doctest_detail_test_suite_ns { \ + static DOCTEST_NOINLINE doctest::detail::TestSuite& getCurrentTestSuite() { \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ + static doctest::detail::TestSuite data; \ + static bool inited = false; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + if(!inited) { \ + data* decorators; \ + inited = true; \ + } \ + return data; \ + } \ + } \ + } \ + namespace ns_name + +#define DOCTEST_TEST_SUITE(decorators) \ + DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(_DOCTEST_ANON_SUITE_)) + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * ""); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for registering exception translators +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ + inline doctest::String translatorName(signature); \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_TRANSLATOR_)) = \ + doctest::registerExceptionTranslator(translatorName); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() \ + doctest::String translatorName(signature) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(_DOCTEST_ANON_TRANSLATOR_), \ + signature) + +// for registering reporters +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_REPORTER_)) = \ + doctest::registerReporter(name, priority, true); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for registering listeners +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_REPORTER_)) = \ + doctest::registerReporter(name, priority, false); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for logging +#define DOCTEST_INFO(expression) \ + DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_), DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_), \ + DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_), expression) + +#define DOCTEST_INFO_IMPL(lambda_name, mb_name, s_name, expression) \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4626) \ + auto lambda_name = [&](std::ostream* s_name) { \ + doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ + mb_name.m_stream = s_name; \ + mb_name << expression; \ + }; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + auto DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope(lambda_name) + +#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := " << x) + +#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, x) \ + do { \ + doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ + mb << x; \ + DOCTEST_ASSERT_LOG_AND_REACT(mb); \ + } while((void)0, 0) + +// clang-format off +#define DOCTEST_ADD_MESSAGE_AT(file, line, x) DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(_DOCTEST_MESSAGE_), x) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, x) DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(_DOCTEST_MESSAGE_), x) +#define DOCTEST_ADD_FAIL_AT(file, line, x) DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(_DOCTEST_MESSAGE_), x) +// clang-format on + +#define DOCTEST_MESSAGE(x) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, x) +#define DOCTEST_FAIL_CHECK(x) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, x) +#define DOCTEST_FAIL(x) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, x) + +#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(_DOCTEST_RB.setResult( \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB) \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + do { \ + DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ + } while((void)0, 0) + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +// necessary for _MESSAGE +#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::decomp_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__) DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) +#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) +#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) +#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) + +// clang-format off +#define DOCTEST_WARN_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); } while((void)0, 0) +#define DOCTEST_CHECK_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); } while((void)0, 0) +#define DOCTEST_REQUIRE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); } while((void)0, 0) +#define DOCTEST_WARN_FALSE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); } while((void)0, 0) +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); } while((void)0, 0) +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); } while((void)0, 0) +// clang-format on + +#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ + do { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr, #__VA_ARGS__, message); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(const doctest::detail::remove_const< \ + doctest::detail::remove_reference<__VA_ARGS__>::type>::type&) { \ + _DOCTEST_RB.translateException(); \ + _DOCTEST_RB.m_threw_as = true; \ + } catch(...) { _DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } \ + } while((void)0, 0) + +#define DOCTEST_ASSERT_THROWS_WITH(expr, assert_type, ...) \ + do { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr, "", __VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(...) { _DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } \ + } while((void)0, 0) + +#define DOCTEST_ASSERT_NOTHROW(expr, assert_type) \ + do { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(...) { _DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } while((void)0, 0) + +// clang-format off +#define DOCTEST_WARN_THROWS(expr) DOCTEST_ASSERT_THROWS_WITH(expr, DT_WARN_THROWS, "") +#define DOCTEST_CHECK_THROWS(expr) DOCTEST_ASSERT_THROWS_WITH(expr, DT_CHECK_THROWS, "") +#define DOCTEST_REQUIRE_THROWS(expr) DOCTEST_ASSERT_THROWS_WITH(expr, DT_REQUIRE_THROWS, "") + +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, DT_WARN_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) + +#define DOCTEST_WARN_NOTHROW(expr) DOCTEST_ASSERT_NOTHROW(expr, DT_WARN_NOTHROW) +#define DOCTEST_CHECK_NOTHROW(expr) DOCTEST_ASSERT_NOTHROW(expr, DT_CHECK_NOTHROW) +#define DOCTEST_REQUIRE_NOTHROW(expr) DOCTEST_ASSERT_NOTHROW(expr, DT_REQUIRE_NOTHROW) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS(expr); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS(expr); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS(expr); } while((void)0, 0) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS_AS(expr, ex); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS_AS(expr, ex); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS_AS(expr, ex); } while((void)0, 0) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS_WITH(expr, with); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS_WITH(expr, with); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS_WITH(expr, with); } while((void)0, 0) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); } while((void)0, 0) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_NOTHROW(expr); } while((void)0, 0) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_NOTHROW(expr); } while((void)0, 0) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_NOTHROW(expr); } while((void)0, 0) +// clang-format on + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ + do { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY( \ + _DOCTEST_RB.binary_assert( \ + __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } while((void)0, 0) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + do { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(_DOCTEST_RB.unary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } while((void)0, 0) + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ + doctest::detail::binary_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \ + #__VA_ARGS__, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) +#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) +#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) +#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) +#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) +#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) +#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) +#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) +#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) +#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) +#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) +#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) +#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) +#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) +#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) +#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) +#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) + +#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS + +#undef DOCTEST_WARN_THROWS +#undef DOCTEST_CHECK_THROWS +#undef DOCTEST_REQUIRE_THROWS +#undef DOCTEST_WARN_THROWS_AS +#undef DOCTEST_CHECK_THROWS_AS +#undef DOCTEST_REQUIRE_THROWS_AS +#undef DOCTEST_WARN_THROWS_WITH +#undef DOCTEST_CHECK_THROWS_WITH +#undef DOCTEST_REQUIRE_THROWS_WITH +#undef DOCTEST_WARN_THROWS_WITH_AS +#undef DOCTEST_CHECK_THROWS_WITH_AS +#undef DOCTEST_REQUIRE_THROWS_WITH_AS +#undef DOCTEST_WARN_NOTHROW +#undef DOCTEST_CHECK_NOTHROW +#undef DOCTEST_REQUIRE_NOTHROW + +#undef DOCTEST_WARN_THROWS_MESSAGE +#undef DOCTEST_CHECK_THROWS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_MESSAGE +#undef DOCTEST_WARN_THROWS_AS_MESSAGE +#undef DOCTEST_CHECK_THROWS_AS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_AS_MESSAGE +#undef DOCTEST_WARN_THROWS_WITH_MESSAGE +#undef DOCTEST_CHECK_THROWS_WITH_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_WITH_MESSAGE +#undef DOCTEST_WARN_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_WARN_NOTHROW_MESSAGE +#undef DOCTEST_CHECK_NOTHROW_MESSAGE +#undef DOCTEST_REQUIRE_NOTHROW_MESSAGE + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#define DOCTEST_WARN_THROWS(expr) ((void)0) +#define DOCTEST_CHECK_THROWS(expr) ((void)0) +#define DOCTEST_REQUIRE_THROWS(expr) ((void)0) +#define DOCTEST_WARN_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_WARN_NOTHROW(expr) ((void)0) +#define DOCTEST_CHECK_NOTHROW(expr) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW(expr) ((void)0) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, msg) ((void)0) + +#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#undef DOCTEST_REQUIRE +#undef DOCTEST_REQUIRE_FALSE +#undef DOCTEST_REQUIRE_MESSAGE +#undef DOCTEST_REQUIRE_FALSE_MESSAGE +#undef DOCTEST_REQUIRE_EQ +#undef DOCTEST_REQUIRE_NE +#undef DOCTEST_REQUIRE_GT +#undef DOCTEST_REQUIRE_LT +#undef DOCTEST_REQUIRE_GE +#undef DOCTEST_REQUIRE_LE +#undef DOCTEST_REQUIRE_UNARY +#undef DOCTEST_REQUIRE_UNARY_FALSE + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// ================================================================================================= +// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == +// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == +// ================================================================================================= +#else // DOCTEST_CONFIG_DISABLE + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ + namespace { \ + template \ + struct der : public base \ + { void f(); }; \ + } \ + template \ + inline void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ + template \ + static inline void f() + +// for registering tests +#define DOCTEST_TEST_CASE(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), name) + +// for registering tests in classes +#define DOCTEST_TEST_CASE_CLASS(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), name) + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(_DOCTEST_ANON_CLASS_), x, \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), name) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING(...) typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) +#define DOCTEST_TYPE_TO_STRING_IMPL(...) + +// for typed tests +#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ + template \ + inline void DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ + template \ + inline void DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for subcases +#define DOCTEST_SUBCASE(name) + +// for a testsuite block +#define DOCTEST_TEST_SUITE(name) namespace + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(name) typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + template \ + static inline doctest::String DOCTEST_ANONYMOUS(_DOCTEST_ANON_TRANSLATOR_)(signature) + +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) + +#define DOCTEST_INFO(x) ((void)0) +#define DOCTEST_CAPTURE(x) ((void)0) +#define DOCTEST_ADD_MESSAGE_AT(file, line, x) ((void)0) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, x) ((void)0) +#define DOCTEST_ADD_FAIL_AT(file, line, x) ((void)0) +#define DOCTEST_MESSAGE(x) ((void)0) +#define DOCTEST_FAIL_CHECK(x) ((void)0) +#define DOCTEST_FAIL(x) ((void)0) + +#define DOCTEST_WARN(...) ((void)0) +#define DOCTEST_CHECK(...) ((void)0) +#define DOCTEST_REQUIRE(...) ((void)0) +#define DOCTEST_WARN_FALSE(...) ((void)0) +#define DOCTEST_CHECK_FALSE(...) ((void)0) +#define DOCTEST_REQUIRE_FALSE(...) ((void)0) + +#define DOCTEST_WARN_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_CHECK_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_REQUIRE_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_WARN_FALSE_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, msg) ((void)0) + +#define DOCTEST_WARN_THROWS(expr) ((void)0) +#define DOCTEST_CHECK_THROWS(expr) ((void)0) +#define DOCTEST_REQUIRE_THROWS(expr) ((void)0) +#define DOCTEST_WARN_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_WARN_NOTHROW(expr) ((void)0) +#define DOCTEST_CHECK_NOTHROW(expr) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW(expr) ((void)0) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, msg) ((void)0) + +#define DOCTEST_WARN_EQ(...) ((void)0) +#define DOCTEST_CHECK_EQ(...) ((void)0) +#define DOCTEST_REQUIRE_EQ(...) ((void)0) +#define DOCTEST_WARN_NE(...) ((void)0) +#define DOCTEST_CHECK_NE(...) ((void)0) +#define DOCTEST_REQUIRE_NE(...) ((void)0) +#define DOCTEST_WARN_GT(...) ((void)0) +#define DOCTEST_CHECK_GT(...) ((void)0) +#define DOCTEST_REQUIRE_GT(...) ((void)0) +#define DOCTEST_WARN_LT(...) ((void)0) +#define DOCTEST_CHECK_LT(...) ((void)0) +#define DOCTEST_REQUIRE_LT(...) ((void)0) +#define DOCTEST_WARN_GE(...) ((void)0) +#define DOCTEST_CHECK_GE(...) ((void)0) +#define DOCTEST_REQUIRE_GE(...) ((void)0) +#define DOCTEST_WARN_LE(...) ((void)0) +#define DOCTEST_CHECK_LE(...) ((void)0) +#define DOCTEST_REQUIRE_LE(...) ((void)0) + +#define DOCTEST_WARN_UNARY(...) ((void)0) +#define DOCTEST_CHECK_UNARY(...) ((void)0) +#define DOCTEST_REQUIRE_UNARY(...) ((void)0) +#define DOCTEST_WARN_UNARY_FALSE(...) ((void)0) +#define DOCTEST_CHECK_UNARY_FALSE(...) ((void)0) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) ((void)0) + +#endif // DOCTEST_CONFIG_DISABLE + +// clang-format off +// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS +#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ +#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ +#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE +#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE +#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE +#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT +#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT +#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT +#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT +#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT +#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT +#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE +#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE +#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE +#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE +#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE +#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE + +#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY +#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY +#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE DOCTEST_TEST_CASE_TEMPLATE_INVOKE +// clang-format on + +// BDD style macros +// clang-format off +#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) +#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) +#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) +#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) + +#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) +#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) +#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE("And when: " name) +#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) +#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) +// clang-format on + +// == SHORT VERSIONS OF THE MACROS +#if !defined(DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES) + +#define TEST_CASE DOCTEST_TEST_CASE +#define TEST_CASE_CLASS DOCTEST_TEST_CASE_CLASS +#define TEST_CASE_FIXTURE DOCTEST_TEST_CASE_FIXTURE +#define TYPE_TO_STRING DOCTEST_TYPE_TO_STRING +#define TEST_CASE_TEMPLATE DOCTEST_TEST_CASE_TEMPLATE +#define TEST_CASE_TEMPLATE_DEFINE DOCTEST_TEST_CASE_TEMPLATE_DEFINE +#define TEST_CASE_TEMPLATE_INVOKE DOCTEST_TEST_CASE_TEMPLATE_INVOKE +#define TEST_CASE_TEMPLATE_APPLY DOCTEST_TEST_CASE_TEMPLATE_APPLY +#define SUBCASE DOCTEST_SUBCASE +#define TEST_SUITE DOCTEST_TEST_SUITE +#define TEST_SUITE_BEGIN DOCTEST_TEST_SUITE_BEGIN +#define TEST_SUITE_END DOCTEST_TEST_SUITE_END +#define REGISTER_EXCEPTION_TRANSLATOR DOCTEST_REGISTER_EXCEPTION_TRANSLATOR +#define REGISTER_REPORTER DOCTEST_REGISTER_REPORTER +#define REGISTER_LISTENER DOCTEST_REGISTER_LISTENER +#define INFO DOCTEST_INFO +#define CAPTURE DOCTEST_CAPTURE +#define ADD_MESSAGE_AT DOCTEST_ADD_MESSAGE_AT +#define ADD_FAIL_CHECK_AT DOCTEST_ADD_FAIL_CHECK_AT +#define ADD_FAIL_AT DOCTEST_ADD_FAIL_AT +#define MESSAGE DOCTEST_MESSAGE +#define FAIL_CHECK DOCTEST_FAIL_CHECK +#define FAIL DOCTEST_FAIL +#define TO_LVALUE DOCTEST_TO_LVALUE + +#define WARN DOCTEST_WARN +#define WARN_FALSE DOCTEST_WARN_FALSE +#define WARN_THROWS DOCTEST_WARN_THROWS +#define WARN_THROWS_AS DOCTEST_WARN_THROWS_AS +#define WARN_THROWS_WITH DOCTEST_WARN_THROWS_WITH +#define WARN_THROWS_WITH_AS DOCTEST_WARN_THROWS_WITH_AS +#define WARN_NOTHROW DOCTEST_WARN_NOTHROW +#define CHECK DOCTEST_CHECK +#define CHECK_FALSE DOCTEST_CHECK_FALSE +#define CHECK_THROWS DOCTEST_CHECK_THROWS +#define CHECK_THROWS_AS DOCTEST_CHECK_THROWS_AS +#define CHECK_THROWS_WITH DOCTEST_CHECK_THROWS_WITH +#define CHECK_THROWS_WITH_AS DOCTEST_CHECK_THROWS_WITH_AS +#define CHECK_NOTHROW DOCTEST_CHECK_NOTHROW +#define REQUIRE DOCTEST_REQUIRE +#define REQUIRE_FALSE DOCTEST_REQUIRE_FALSE +#define REQUIRE_THROWS DOCTEST_REQUIRE_THROWS +#define REQUIRE_THROWS_AS DOCTEST_REQUIRE_THROWS_AS +#define REQUIRE_THROWS_WITH DOCTEST_REQUIRE_THROWS_WITH +#define REQUIRE_THROWS_WITH_AS DOCTEST_REQUIRE_THROWS_WITH_AS +#define REQUIRE_NOTHROW DOCTEST_REQUIRE_NOTHROW + +#define WARN_MESSAGE DOCTEST_WARN_MESSAGE +#define WARN_FALSE_MESSAGE DOCTEST_WARN_FALSE_MESSAGE +#define WARN_THROWS_MESSAGE DOCTEST_WARN_THROWS_MESSAGE +#define WARN_THROWS_AS_MESSAGE DOCTEST_WARN_THROWS_AS_MESSAGE +#define WARN_THROWS_WITH_MESSAGE DOCTEST_WARN_THROWS_WITH_MESSAGE +#define WARN_THROWS_WITH_AS_MESSAGE DOCTEST_WARN_THROWS_WITH_AS_MESSAGE +#define WARN_NOTHROW_MESSAGE DOCTEST_WARN_NOTHROW_MESSAGE +#define CHECK_MESSAGE DOCTEST_CHECK_MESSAGE +#define CHECK_FALSE_MESSAGE DOCTEST_CHECK_FALSE_MESSAGE +#define CHECK_THROWS_MESSAGE DOCTEST_CHECK_THROWS_MESSAGE +#define CHECK_THROWS_AS_MESSAGE DOCTEST_CHECK_THROWS_AS_MESSAGE +#define CHECK_THROWS_WITH_MESSAGE DOCTEST_CHECK_THROWS_WITH_MESSAGE +#define CHECK_THROWS_WITH_AS_MESSAGE DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE +#define CHECK_NOTHROW_MESSAGE DOCTEST_CHECK_NOTHROW_MESSAGE +#define REQUIRE_MESSAGE DOCTEST_REQUIRE_MESSAGE +#define REQUIRE_FALSE_MESSAGE DOCTEST_REQUIRE_FALSE_MESSAGE +#define REQUIRE_THROWS_MESSAGE DOCTEST_REQUIRE_THROWS_MESSAGE +#define REQUIRE_THROWS_AS_MESSAGE DOCTEST_REQUIRE_THROWS_AS_MESSAGE +#define REQUIRE_THROWS_WITH_MESSAGE DOCTEST_REQUIRE_THROWS_WITH_MESSAGE +#define REQUIRE_THROWS_WITH_AS_MESSAGE DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE +#define REQUIRE_NOTHROW_MESSAGE DOCTEST_REQUIRE_NOTHROW_MESSAGE + +#define SCENARIO DOCTEST_SCENARIO +#define SCENARIO_CLASS DOCTEST_SCENARIO_CLASS +#define SCENARIO_TEMPLATE DOCTEST_SCENARIO_TEMPLATE +#define SCENARIO_TEMPLATE_DEFINE DOCTEST_SCENARIO_TEMPLATE_DEFINE +#define GIVEN DOCTEST_GIVEN +#define WHEN DOCTEST_WHEN +#define AND_WHEN DOCTEST_AND_WHEN +#define THEN DOCTEST_THEN +#define AND_THEN DOCTEST_AND_THEN + +#define WARN_EQ DOCTEST_WARN_EQ +#define CHECK_EQ DOCTEST_CHECK_EQ +#define REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define WARN_NE DOCTEST_WARN_NE +#define CHECK_NE DOCTEST_CHECK_NE +#define REQUIRE_NE DOCTEST_REQUIRE_NE +#define WARN_GT DOCTEST_WARN_GT +#define CHECK_GT DOCTEST_CHECK_GT +#define REQUIRE_GT DOCTEST_REQUIRE_GT +#define WARN_LT DOCTEST_WARN_LT +#define CHECK_LT DOCTEST_CHECK_LT +#define REQUIRE_LT DOCTEST_REQUIRE_LT +#define WARN_GE DOCTEST_WARN_GE +#define CHECK_GE DOCTEST_CHECK_GE +#define REQUIRE_GE DOCTEST_REQUIRE_GE +#define WARN_LE DOCTEST_WARN_LE +#define CHECK_LE DOCTEST_CHECK_LE +#define REQUIRE_LE DOCTEST_REQUIRE_LE +#define WARN_UNARY DOCTEST_WARN_UNARY +#define CHECK_UNARY DOCTEST_CHECK_UNARY +#define REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +// KEPT FOR BACKWARDS COMPATIBILITY +#define FAST_WARN_EQ DOCTEST_FAST_WARN_EQ +#define FAST_CHECK_EQ DOCTEST_FAST_CHECK_EQ +#define FAST_REQUIRE_EQ DOCTEST_FAST_REQUIRE_EQ +#define FAST_WARN_NE DOCTEST_FAST_WARN_NE +#define FAST_CHECK_NE DOCTEST_FAST_CHECK_NE +#define FAST_REQUIRE_NE DOCTEST_FAST_REQUIRE_NE +#define FAST_WARN_GT DOCTEST_FAST_WARN_GT +#define FAST_CHECK_GT DOCTEST_FAST_CHECK_GT +#define FAST_REQUIRE_GT DOCTEST_FAST_REQUIRE_GT +#define FAST_WARN_LT DOCTEST_FAST_WARN_LT +#define FAST_CHECK_LT DOCTEST_FAST_CHECK_LT +#define FAST_REQUIRE_LT DOCTEST_FAST_REQUIRE_LT +#define FAST_WARN_GE DOCTEST_FAST_WARN_GE +#define FAST_CHECK_GE DOCTEST_FAST_CHECK_GE +#define FAST_REQUIRE_GE DOCTEST_FAST_REQUIRE_GE +#define FAST_WARN_LE DOCTEST_FAST_WARN_LE +#define FAST_CHECK_LE DOCTEST_FAST_CHECK_LE +#define FAST_REQUIRE_LE DOCTEST_FAST_REQUIRE_LE + +#define FAST_WARN_UNARY DOCTEST_FAST_WARN_UNARY +#define FAST_CHECK_UNARY DOCTEST_FAST_CHECK_UNARY +#define FAST_REQUIRE_UNARY DOCTEST_FAST_REQUIRE_UNARY +#define FAST_WARN_UNARY_FALSE DOCTEST_FAST_WARN_UNARY_FALSE +#define FAST_CHECK_UNARY_FALSE DOCTEST_FAST_CHECK_UNARY_FALSE +#define FAST_REQUIRE_UNARY_FALSE DOCTEST_FAST_REQUIRE_UNARY_FALSE + +#define TEST_CASE_TEMPLATE_INSTANTIATE DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE + +#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#if !defined(DOCTEST_CONFIG_DISABLE) + +// this is here to clear the 'current test suite' for the current translation unit - at the top +DOCTEST_TEST_SUITE_END(); + +// add stringification for primitive/fundamental types +namespace doctest { namespace detail { + DOCTEST_TYPE_TO_STRING_IMPL(bool) + DOCTEST_TYPE_TO_STRING_IMPL(float) + DOCTEST_TYPE_TO_STRING_IMPL(double) + DOCTEST_TYPE_TO_STRING_IMPL(long double) + DOCTEST_TYPE_TO_STRING_IMPL(char) + DOCTEST_TYPE_TO_STRING_IMPL(signed char) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned char) +#if !DOCTEST_MSVC || defined(_NATIVE_WCHAR_T_DEFINED) + DOCTEST_TYPE_TO_STRING_IMPL(wchar_t) +#endif // not MSVC or wchar_t support enabled + DOCTEST_TYPE_TO_STRING_IMPL(short int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned short int) + DOCTEST_TYPE_TO_STRING_IMPL(int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned int) + DOCTEST_TYPE_TO_STRING_IMPL(long int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned long int) + DOCTEST_TYPE_TO_STRING_IMPL(long long int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned long long int) +}} // namespace doctest::detail + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_LIBRARY_INCLUDED + +#ifndef DOCTEST_SINGLE_HEADER +#define DOCTEST_SINGLE_HEADER +#endif // DOCTEST_SINGLE_HEADER + +#if defined(DOCTEST_CONFIG_IMPLEMENT) || !defined(DOCTEST_SINGLE_HEADER) + +#ifndef DOCTEST_SINGLE_HEADER +#include "doctest_fwd.h" +#endif // DOCTEST_SINGLE_HEADER + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") + +#ifndef DOCTEST_LIBRARY_IMPLEMENTATION +#define DOCTEST_LIBRARY_IMPLEMENTATION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-local-typedef") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-local-typedefs") +DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4616) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4619) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4996) // The compiler encountered a deprecated declaration +DOCTEST_MSVC_SUPPRESS_WARNING(4267) // 'var' : conversion from 'x' to 'y', possible loss of data +DOCTEST_MSVC_SUPPRESS_WARNING(4706) // assignment within conditional expression +DOCTEST_MSVC_SUPPRESS_WARNING(4512) // 'class' : assignment operator could not be generated +DOCTEST_MSVC_SUPPRESS_WARNING(4127) // conditional expression is constant +DOCTEST_MSVC_SUPPRESS_WARNING(4530) // C++ exception handler used, but unwind semantics not enabled +DOCTEST_MSVC_SUPPRESS_WARNING(4577) // 'noexcept' used with no exception handling mode specified +DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string expected in argument is not a string literal +DOCTEST_MSVC_SUPPRESS_WARNING(4365) // conversion from 'int' to 'unsigned', signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4820) // padding in structs +DOCTEST_MSVC_SUPPRESS_WARNING(4640) // construction of local static object is not thread-safe +DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to potentially throwing function passed to extern C +DOCTEST_MSVC_SUPPRESS_WARNING(5045) // Spectre mitigation stuff +DOCTEST_MSVC_SUPPRESS_WARNING(4626) // assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5027) // move assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5026) // move constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4625) // copy constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4800) // forcing value to bool 'true' or 'false' (performance warning) +// static analysis +DOCTEST_MSVC_SUPPRESS_WARNING(26439) // This kind of function may not throw. Declare it 'noexcept' +DOCTEST_MSVC_SUPPRESS_WARNING(26495) // Always initialize a member variable +DOCTEST_MSVC_SUPPRESS_WARNING(26451) // Arithmetic overflow ... +DOCTEST_MSVC_SUPPRESS_WARNING(26444) // Avoid unnamed objects with custom construction and dtor... +DOCTEST_MSVC_SUPPRESS_WARNING(26812) // Prefer 'enum class' over 'enum' + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN + +// required includes - will go only in one translation unit! +#include +#include +#include +// borland (Embarcadero) compiler requires math.h and not cmath - https://github.com/onqtam/doctest/pull/37 +#ifdef __BORLANDC__ +#include +#endif // __BORLANDC__ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DOCTEST_CONFIG_POSIX_SIGNALS +#include +#endif // DOCTEST_CONFIG_POSIX_SIGNALS +#include +#include +#include + +#ifdef DOCTEST_PLATFORM_MAC +#include +#include +#include +#endif // DOCTEST_PLATFORM_MAC + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// defines for a leaner windows.h +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif // WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX + +// not sure what AfxWin.h is for - here I do what Catch does +#ifdef __AFXDLL +#include +#else +#include +#endif +#include + +#else // DOCTEST_PLATFORM_WINDOWS + +#include +#include + +#endif // DOCTEST_PLATFORM_WINDOWS + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END + +// counts the number of elements in a C array +#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) + +#ifdef DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled +#else // DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled +#endif // DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX +#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" +#endif + +#ifndef DOCTEST_THREAD_LOCAL +#define DOCTEST_THREAD_LOCAL thread_local +#endif + +#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS +#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX +#else +#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" +#endif + +namespace doctest { + +bool is_running_in_test = false; + +namespace { + using namespace detail; + // case insensitive strcmp + int stricmp(const char* a, const char* b) { + for(;; a++, b++) { + const int d = tolower(*a) - tolower(*b); + if(d != 0 || !*a) + return d; + } + } + + template + String fpToString(T value, int precision) { + std::ostringstream oss; + oss << std::setprecision(precision) << std::fixed << value; + std::string d = oss.str(); + size_t i = d.find_last_not_of('0'); + if(i != std::string::npos && i != d.size() - 1) { + if(d[i] == '.') + i++; + d = d.substr(0, i + 1); + } + return d.c_str(); + } + + struct Endianness + { + enum Arch + { + Big, + Little + }; + + static Arch which() { + int x = 1; + // casting any data pointer to char* is allowed + auto ptr = reinterpret_cast(&x); + if(*ptr) + return Little; + return Big; + } + }; +} // namespace + +namespace detail { + void my_memcpy(void* dest, const void* src, unsigned num) { memcpy(dest, src, num); } + + String rawMemoryToString(const void* object, unsigned size) { + // Reverse order for little endian architectures + int i = 0, end = static_cast(size), inc = 1; + if(Endianness::which() == Endianness::Little) { + i = end - 1; + end = inc = -1; + } + + unsigned const char* bytes = static_cast(object); + std::ostringstream oss; + oss << "0x" << std::setfill('0') << std::hex; + for(; i != end; i += inc) + oss << std::setw(2) << static_cast(bytes[i]); + return oss.str().c_str(); + } + + DOCTEST_THREAD_LOCAL std::ostringstream g_oss; // NOLINT(cert-err58-cpp) + + std::ostream* getTlsOss() { + g_oss.clear(); // there shouldn't be anything worth clearing in the flags + g_oss.str(""); // the slow way of resetting a string stream + //g_oss.seekp(0); // optimal reset - as seen here: https://stackoverflow.com/a/624291/3162383 + return &g_oss; + } + + String getTlsOssResult() { + //g_oss << std::ends; // needed - as shown here: https://stackoverflow.com/a/624291/3162383 + return g_oss.str().c_str(); + } + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace timer_large_integer +{ + +#if defined(DOCTEST_PLATFORM_WINDOWS) + typedef ULONGLONG type; +#else // DOCTEST_PLATFORM_WINDOWS + using namespace std; + typedef uint64_t type; +#endif // DOCTEST_PLATFORM_WINDOWS +} + +typedef timer_large_integer::type ticks_t; + +#ifdef DOCTEST_CONFIG_GETCURRENTTICKS + ticks_t getCurrentTicks() { return DOCTEST_CONFIG_GETCURRENTTICKS(); } +#elif defined(DOCTEST_PLATFORM_WINDOWS) + ticks_t getCurrentTicks() { + static LARGE_INTEGER hz = {0}, hzo = {0}; + if(!hz.QuadPart) { + QueryPerformanceFrequency(&hz); + QueryPerformanceCounter(&hzo); + } + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; + } +#else // DOCTEST_PLATFORM_WINDOWS + ticks_t getCurrentTicks() { + timeval t; + gettimeofday(&t, nullptr); + return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); + } +#endif // DOCTEST_PLATFORM_WINDOWS + + struct Timer + { + void start() { m_ticks = getCurrentTicks(); } + unsigned int getElapsedMicroseconds() const { + return static_cast(getCurrentTicks() - m_ticks); + } + //unsigned int getElapsedMilliseconds() const { + // return static_cast(getElapsedMicroseconds() / 1000); + //} + double getElapsedSeconds() const { return (getCurrentTicks() - m_ticks) / 1000000.0; } + + private: + ticks_t m_ticks = 0; + }; + + // this holds both parameters from the command line and runtime data for tests + struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats + { + std::atomic numAssertsCurrentTest_atomic; + std::atomic numAssertsFailedCurrentTest_atomic; + + std::vector> filters = decltype(filters)(9); // 9 different filters + + std::vector reporters_currently_used; + + const TestCase* currentTest = nullptr; + + assert_handler ah = nullptr; + + Timer timer; + + std::vector stringifiedContexts; // logging from INFO() due to an exception + + // stuff for subcases + std::vector subcasesStack; + std::set subcasesPassed; + int subcasesCurrentMaxLevel; + bool should_reenter; + std::atomic shouldLogCurrentException; + + void resetRunData() { + numTestCases = 0; + numTestCasesPassingFilters = 0; + numTestSuitesPassingFilters = 0; + numTestCasesFailed = 0; + numAsserts = 0; + numAssertsFailed = 0; + numAssertsCurrentTest = 0; + numAssertsFailedCurrentTest = 0; + } + + void finalizeTestCaseData() { + seconds = timer.getElapsedSeconds(); + + // update the non-atomic counters + numAsserts += numAssertsCurrentTest_atomic; + numAssertsFailed += numAssertsFailedCurrentTest_atomic; + numAssertsCurrentTest = numAssertsCurrentTest_atomic; + numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; + + if(numAssertsFailedCurrentTest) + failure_flags |= TestCaseFailureReason::AssertFailure; + + if(Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && + Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) + failure_flags |= TestCaseFailureReason::Timeout; + + if(currentTest->m_should_fail) { + if(failure_flags) { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; + } else { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; + } + } else if(failure_flags && currentTest->m_may_fail) { + failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; + } else if(currentTest->m_expected_failures > 0) { + if(numAssertsFailedCurrentTest == currentTest->m_expected_failures) { + failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; + } else { + failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; + } + } + + bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); + + // if any subcase has failed - the whole test case has failed + if(failure_flags && !ok_to_fail) + numTestCasesFailed++; + } + }; + + ContextState* g_cs = nullptr; + + // used to avoid locks for the debug output + // TODO: figure out if this is indeed necessary/correct - seems like either there still + // could be a race or that there wouldn't be a race even if using the context directly + DOCTEST_THREAD_LOCAL bool g_no_colors; + +#endif // DOCTEST_CONFIG_DISABLE +} // namespace detail + +void String::setOnHeap() { *reinterpret_cast(&buf[last]) = 128; } +void String::setLast(unsigned in) { buf[last] = char(in); } + +void String::copy(const String& other) { + using namespace std; + if(other.isOnStack()) { + memcpy(buf, other.buf, len); + } else { + setOnHeap(); + data.size = other.data.size; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + memcpy(data.ptr, other.data.ptr, data.size + 1); + } +} + +String::String() { + buf[0] = '\0'; + setLast(); +} + +String::~String() { + if(!isOnStack()) + delete[] data.ptr; +} + +String::String(const char* in) + : String(in, strlen(in)) {} + +String::String(const char* in, unsigned in_size) { + using namespace std; + if(in_size <= last) { + memcpy(buf, in, in_size + 1); + setLast(last - in_size); + } else { + setOnHeap(); + data.size = in_size; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + memcpy(data.ptr, in, in_size + 1); + } +} + +String::String(const String& other) { copy(other); } + +String& String::operator=(const String& other) { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + + copy(other); + } + + return *this; +} + +String& String::operator+=(const String& other) { + const unsigned my_old_size = size(); + const unsigned other_size = other.size(); + const unsigned total_size = my_old_size + other_size; + using namespace std; + if(isOnStack()) { + if(total_size < len) { + // append to the current stack space + memcpy(buf + my_old_size, other.c_str(), other_size + 1); + setLast(last - total_size); + } else { + // alloc new chunk + char* temp = new char[total_size + 1]; + // copy current data to new location before writing in the union + memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed + // update data in union + setOnHeap(); + data.size = total_size; + data.capacity = data.size + 1; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } else { + if(data.capacity > total_size) { + // append to the current heap block + data.size = total_size; + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } else { + // resize + data.capacity *= 2; + if(data.capacity <= total_size) + data.capacity = total_size + 1; + // alloc new chunk + char* temp = new char[data.capacity]; + // copy current data to new location before releasing it + memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed + // release old chunk + delete[] data.ptr; + // update the rest of the union members + data.size = total_size; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } + + return *this; +} + +String String::operator+(const String& other) const { return String(*this) += other; } + +String::String(String&& other) { + using namespace std; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); +} + +String& String::operator=(String&& other) { + using namespace std; + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); + } + return *this; +} + +char String::operator[](unsigned i) const { + return const_cast(this)->operator[](i); // NOLINT +} + +char& String::operator[](unsigned i) { + if(isOnStack()) + return reinterpret_cast(buf)[i]; + return data.ptr[i]; +} + +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") +unsigned String::size() const { + if(isOnStack()) + return last - (unsigned(buf[last]) & 31); // using "last" would work only if "len" is 32 + return data.size; +} +DOCTEST_GCC_SUPPRESS_WARNING_POP + +unsigned String::capacity() const { + if(isOnStack()) + return len; + return data.capacity; +} + +int String::compare(const char* other, bool no_case) const { + if(no_case) + return doctest::stricmp(c_str(), other); + return std::strcmp(c_str(), other); +} + +int String::compare(const String& other, bool no_case) const { + return compare(other.c_str(), no_case); +} + +// clang-format off +bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; } +bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; } +bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; } +bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; } +bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; } +bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; } +// clang-format on + +std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); } + +namespace { + void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) +} // namespace + +namespace Color { + std::ostream& operator<<(std::ostream& s, Color::Enum code) { + color_to_stream(s, code); + return s; + } +} // namespace Color + +// clang-format off +const char* assertString(assertType::Enum at) { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4062) // enum 'x' in switch of enum 'y' is not handled + switch(at) { //!OCLINT missing default in switch statements + case assertType::DT_WARN : return "WARN"; + case assertType::DT_CHECK : return "CHECK"; + case assertType::DT_REQUIRE : return "REQUIRE"; + + case assertType::DT_WARN_FALSE : return "WARN_FALSE"; + case assertType::DT_CHECK_FALSE : return "CHECK_FALSE"; + case assertType::DT_REQUIRE_FALSE : return "REQUIRE_FALSE"; + + case assertType::DT_WARN_THROWS : return "WARN_THROWS"; + case assertType::DT_CHECK_THROWS : return "CHECK_THROWS"; + case assertType::DT_REQUIRE_THROWS : return "REQUIRE_THROWS"; + + case assertType::DT_WARN_THROWS_AS : return "WARN_THROWS_AS"; + case assertType::DT_CHECK_THROWS_AS : return "CHECK_THROWS_AS"; + case assertType::DT_REQUIRE_THROWS_AS : return "REQUIRE_THROWS_AS"; + + case assertType::DT_WARN_THROWS_WITH : return "WARN_THROWS_WITH"; + case assertType::DT_CHECK_THROWS_WITH : return "CHECK_THROWS_WITH"; + case assertType::DT_REQUIRE_THROWS_WITH : return "REQUIRE_THROWS_WITH"; + + case assertType::DT_WARN_THROWS_WITH_AS : return "WARN_THROWS_WITH_AS"; + case assertType::DT_CHECK_THROWS_WITH_AS : return "CHECK_THROWS_WITH_AS"; + case assertType::DT_REQUIRE_THROWS_WITH_AS : return "REQUIRE_THROWS_WITH_AS"; + + case assertType::DT_WARN_NOTHROW : return "WARN_NOTHROW"; + case assertType::DT_CHECK_NOTHROW : return "CHECK_NOTHROW"; + case assertType::DT_REQUIRE_NOTHROW : return "REQUIRE_NOTHROW"; + + case assertType::DT_WARN_EQ : return "WARN_EQ"; + case assertType::DT_CHECK_EQ : return "CHECK_EQ"; + case assertType::DT_REQUIRE_EQ : return "REQUIRE_EQ"; + case assertType::DT_WARN_NE : return "WARN_NE"; + case assertType::DT_CHECK_NE : return "CHECK_NE"; + case assertType::DT_REQUIRE_NE : return "REQUIRE_NE"; + case assertType::DT_WARN_GT : return "WARN_GT"; + case assertType::DT_CHECK_GT : return "CHECK_GT"; + case assertType::DT_REQUIRE_GT : return "REQUIRE_GT"; + case assertType::DT_WARN_LT : return "WARN_LT"; + case assertType::DT_CHECK_LT : return "CHECK_LT"; + case assertType::DT_REQUIRE_LT : return "REQUIRE_LT"; + case assertType::DT_WARN_GE : return "WARN_GE"; + case assertType::DT_CHECK_GE : return "CHECK_GE"; + case assertType::DT_REQUIRE_GE : return "REQUIRE_GE"; + case assertType::DT_WARN_LE : return "WARN_LE"; + case assertType::DT_CHECK_LE : return "CHECK_LE"; + case assertType::DT_REQUIRE_LE : return "REQUIRE_LE"; + + case assertType::DT_WARN_UNARY : return "WARN_UNARY"; + case assertType::DT_CHECK_UNARY : return "CHECK_UNARY"; + case assertType::DT_REQUIRE_UNARY : return "REQUIRE_UNARY"; + case assertType::DT_WARN_UNARY_FALSE : return "WARN_UNARY_FALSE"; + case assertType::DT_CHECK_UNARY_FALSE : return "CHECK_UNARY_FALSE"; + case assertType::DT_REQUIRE_UNARY_FALSE : return "REQUIRE_UNARY_FALSE"; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + return ""; +} +// clang-format on + +const char* failureString(assertType::Enum at) { + if(at & assertType::is_warn) //!OCLINT bitwise operator in conditional + return "WARNING"; + if(at & assertType::is_check) //!OCLINT bitwise operator in conditional + return "ERROR"; + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return "FATAL ERROR"; + return ""; +} + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +// depending on the current options this will remove the path of filenames +const char* skipPathFromFilename(const char* file) { + if(getContextOptions()->no_path_in_filenames) { + auto back = std::strrchr(file, '\\'); + auto forward = std::strrchr(file, '/'); + if(back || forward) { + if(back > forward) + forward = back; + return forward + 1; + } + } + return file; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +bool SubcaseSignature::operator<(const SubcaseSignature& other) const { + if(m_line != other.m_line) + return m_line < other.m_line; + if(std::strcmp(m_file, other.m_file) != 0) + return std::strcmp(m_file, other.m_file) < 0; + return m_name.compare(other.m_name) < 0; +} + +IContextScope::IContextScope() = default; +IContextScope::~IContextScope() = default; + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(char* in) { return toString(static_cast(in)); } +String toString(const char* in) { return String("\"") + (in ? in : "{null string}") + "\""; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(bool in) { return in ? "true" : "false"; } +String toString(float in) { return fpToString(in, 5) + "f"; } +String toString(double in) { return fpToString(in, 10); } +String toString(double long in) { return fpToString(in, 15); } + +#define DOCTEST_TO_STRING_OVERLOAD(type, fmt) \ + String toString(type in) { \ + char buf[64]; \ + std::sprintf(buf, fmt, in); \ + return buf; \ + } + +DOCTEST_TO_STRING_OVERLOAD(char, "%d") +DOCTEST_TO_STRING_OVERLOAD(char signed, "%d") +DOCTEST_TO_STRING_OVERLOAD(char unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int short, "%d") +DOCTEST_TO_STRING_OVERLOAD(int short unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int, "%d") +DOCTEST_TO_STRING_OVERLOAD(unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int long, "%ld") +DOCTEST_TO_STRING_OVERLOAD(int long unsigned, "%lu") +DOCTEST_TO_STRING_OVERLOAD(int long long, "%lld") +DOCTEST_TO_STRING_OVERLOAD(int long long unsigned, "%llu") + +String toString(std::nullptr_t) { return "NULL"; } + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/onqtam/doctest/issues/183 +String toString(const std::string& in) { return in.c_str(); } +#endif // VS 2019 + +Approx::Approx(double value) + : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100) + , m_scale(1.0) + , m_value(value) {} + +Approx Approx::operator()(double value) const { + Approx approx(value); + approx.epsilon(m_epsilon); + approx.scale(m_scale); + return approx; +} + +Approx& Approx::epsilon(double newEpsilon) { + m_epsilon = newEpsilon; + return *this; +} +Approx& Approx::scale(double newScale) { + m_scale = newScale; + return *this; +} + +bool operator==(double lhs, const Approx& rhs) { + // Thanks to Richard Harris for his help refining this formula + return std::fabs(lhs - rhs.m_value) < + rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); +} +bool operator==(const Approx& lhs, double rhs) { return operator==(rhs, lhs); } +bool operator!=(double lhs, const Approx& rhs) { return !operator==(lhs, rhs); } +bool operator!=(const Approx& lhs, double rhs) { return !operator==(rhs, lhs); } +bool operator<=(double lhs, const Approx& rhs) { return lhs < rhs.m_value || lhs == rhs; } +bool operator<=(const Approx& lhs, double rhs) { return lhs.m_value < rhs || lhs == rhs; } +bool operator>=(double lhs, const Approx& rhs) { return lhs > rhs.m_value || lhs == rhs; } +bool operator>=(const Approx& lhs, double rhs) { return lhs.m_value > rhs || lhs == rhs; } +bool operator<(double lhs, const Approx& rhs) { return lhs < rhs.m_value && lhs != rhs; } +bool operator<(const Approx& lhs, double rhs) { return lhs.m_value < rhs && lhs != rhs; } +bool operator>(double lhs, const Approx& rhs) { return lhs > rhs.m_value && lhs != rhs; } +bool operator>(const Approx& lhs, double rhs) { return lhs.m_value > rhs && lhs != rhs; } + +String toString(const Approx& in) { + return String("Approx( ") + doctest::toString(in.m_value) + " )"; +} +const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); } + +} // namespace doctest + +#ifdef DOCTEST_CONFIG_DISABLE +namespace doctest { +Context::Context(int, const char* const*) {} +Context::~Context() = default; +void Context::applyCommandLine(int, const char* const*) {} +void Context::addFilter(const char*, const char*) {} +void Context::clearFilters() {} +void Context::setOption(const char*, int) {} +void Context::setOption(const char*, const char*) {} +bool Context::shouldExit() { return false; } +void Context::setAsDefaultForAssertsOutOfTestCases() {} +void Context::setAssertHandler(detail::assert_handler) {} +int Context::run() { return 0; } + +IReporter::~IReporter() = default; + +int IReporter::get_num_active_contexts() { return 0; } +const IContextScope* const* IReporter::get_active_contexts() { return nullptr; } +int IReporter::get_num_stringified_contexts() { return 0; } +const String* IReporter::get_stringified_contexts() { return nullptr; } + +int registerReporter(const char*, int, IReporter*) { return 0; } + +} // namespace doctest +#else // DOCTEST_CONFIG_DISABLE + +#if !defined(DOCTEST_CONFIG_COLORS_NONE) +#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_CONFIG_COLORS_WINDOWS +#else // linux +#define DOCTEST_CONFIG_COLORS_ANSI +#endif // platform +#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI +#endif // DOCTEST_CONFIG_COLORS_NONE + +namespace doctest_detail_test_suite_ns { +// holds the current test suite +doctest::detail::TestSuite& getCurrentTestSuite() { + static doctest::detail::TestSuite data; + return data; +} +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +namespace { + // the int (priority) is part of the key for automatic sorting - sadly one can register a + // reporter with a duplicate name and a different priority but hopefully that won't happen often :| + typedef std::map, reporterCreatorFunc> reporterMap; + + reporterMap& getReporters() { + static reporterMap data; + return data; + } + reporterMap& getListeners() { + static reporterMap data; + return data; + } +} // namespace +namespace detail { +#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ + for(auto& curr_rep : g_cs->reporters_currently_used) \ + curr_rep->function(__VA_ARGS__) + + bool checkIfShouldThrow(assertType::Enum at) { + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return true; + + if((at & assertType::is_check) //!OCLINT bitwise operator in conditional + && getContextOptions()->abort_after > 0 && + (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= + getContextOptions()->abort_after) + return true; + + return false; + } + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + [[noreturn]] void throwException() { + g_cs->shouldLogCurrentException = false; + throw TestFailureException(); + } // NOLINT(cert-err60-cpp) +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + void throwException() {} +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} // namespace detail + +namespace { + using namespace detail; + // matching of a string against a wildcard mask (case sensitivity configurable) taken from + // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing + int wildcmp(const char* str, const char* wild, bool caseSensitive) { + const char* cp = nullptr; + const char* mp = nullptr; + + while((*str) && (*wild != '*')) { + if((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && + (*wild != '?')) { + return 0; + } + wild++; + str++; + } + + while(*str) { + if(*wild == '*') { + if(!*++wild) { + return 1; + } + mp = wild; + cp = str + 1; + } else if((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || + (*wild == '?')) { + wild++; + str++; + } else { + wild = mp; //!OCLINT parameter reassignment + str = cp++; //!OCLINT parameter reassignment + } + } + + while(*wild == '*') { + wild++; + } + return !*wild; + } + + //// C string hash function (djb2) - taken from http://www.cse.yorku.ca/~oz/hash.html + //unsigned hashStr(unsigned const char* str) { + // unsigned long hash = 5381; + // char c; + // while((c = *str++)) + // hash = ((hash << 5) + hash) + c; // hash * 33 + c + // return hash; + //} + + // checks if the name matches any of the filters (and can be configured what to do when empty) + bool matchesAny(const char* name, const std::vector& filters, bool matchEmpty, + bool caseSensitive) { + if(filters.empty() && matchEmpty) + return true; + for(auto& curr : filters) + if(wildcmp(name, curr.c_str(), caseSensitive)) + return true; + return false; + } +} // namespace +namespace detail { + + Subcase::Subcase(const String& name, const char* file, int line) + : m_signature({name, file, line}) { + ContextState* s = g_cs; + + // check subcase filters + if(s->subcasesStack.size() < size_t(s->subcase_filter_levels)) { + if(!matchesAny(m_signature.m_name.c_str(), s->filters[6], true, s->case_sensitive)) + return; + if(matchesAny(m_signature.m_name.c_str(), s->filters[7], false, s->case_sensitive)) + return; + } + + // if a Subcase on the same level has already been entered + if(s->subcasesStack.size() < size_t(s->subcasesCurrentMaxLevel)) { + s->should_reenter = true; + return; + } + + // push the current signature to the stack so we can check if the + // current stack + the current new subcase have been traversed + s->subcasesStack.push_back(m_signature); + if(s->subcasesPassed.count(s->subcasesStack) != 0) { + // pop - revert to previous stack since we've already passed this + s->subcasesStack.pop_back(); + return; + } + + s->subcasesCurrentMaxLevel = s->subcasesStack.size(); + m_entered = true; + + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); + } + + Subcase::~Subcase() { + if(m_entered) { + // only mark the subcase stack as passed if no subcases have been skipped + if(g_cs->should_reenter == false) + g_cs->subcasesPassed.insert(g_cs->subcasesStack); + g_cs->subcasesStack.pop_back(); + +#if __cplusplus >= 201703L && defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411 + if(std::uncaught_exceptions() > 0 +#else + if(std::uncaught_exception() +#endif + && g_cs->shouldLogCurrentException) { + DOCTEST_ITERATE_THROUGH_REPORTERS( + test_case_exception, {"exception thrown in subcase - will translate later " + "when the whole test case has been exited (cannot " + "translate while there is an active exception)", + false}); + g_cs->shouldLogCurrentException = false; + } + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + } + + Subcase::operator bool() const { return m_entered; } + + Result::Result(bool passed, const String& decomposition) + : m_passed(passed) + , m_decomp(decomposition) {} + + ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) + : m_at(at) {} + + TestSuite& TestSuite::operator*(const char* in) { + m_test_suite = in; + // clear state + m_description = nullptr; + m_skip = false; + m_may_fail = false; + m_should_fail = false; + m_expected_failures = 0; + m_timeout = 0; + return *this; + } + + TestCase::TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const char* type, int template_id) { + m_file = file; + m_line = line; + m_name = nullptr; // will be later overridden in operator* + m_test_suite = test_suite.m_test_suite; + m_description = test_suite.m_description; + m_skip = test_suite.m_skip; + m_may_fail = test_suite.m_may_fail; + m_should_fail = test_suite.m_should_fail; + m_expected_failures = test_suite.m_expected_failures; + m_timeout = test_suite.m_timeout; + + m_test = test; + m_type = type; + m_template_id = template_id; + } + + TestCase::TestCase(const TestCase& other) + : TestCaseData() { + *this = other; + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + DOCTEST_MSVC_SUPPRESS_WARNING(26437) // Do not slice + TestCase& TestCase::operator=(const TestCase& other) { + static_cast(*this) = static_cast(other); + + m_test = other.m_test; + m_type = other.m_type; + m_template_id = other.m_template_id; + m_full_name = other.m_full_name; + + if(m_template_id != -1) + m_name = m_full_name.c_str(); + return *this; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& TestCase::operator*(const char* in) { + m_name = in; + // make a new name with an appended type for templated test case + if(m_template_id != -1) { + m_full_name = String(m_name) + m_type; + // redirect the name to point to the newly constructed full name + m_name = m_full_name.c_str(); + } + return *this; + } + + bool TestCase::operator<(const TestCase& other) const { + if(m_line != other.m_line) + return m_line < other.m_line; + const int file_cmp = std::strcmp(m_file, other.m_file); + if(file_cmp != 0) + return file_cmp < 0; + return m_template_id < other.m_template_id; + } +} // namespace detail +namespace { + using namespace detail; + // for sorting tests by file/line + bool fileOrderComparator(const TestCase* lhs, const TestCase* rhs) { +#if DOCTEST_MSVC + // this is needed because MSVC gives different case for drive letters + // for __FILE__ when evaluated in a header and a source file + const int res = doctest::stricmp(lhs->m_file, rhs->m_file); +#else // MSVC + const int res = std::strcmp(lhs->m_file, rhs->m_file); +#endif // MSVC + if(res != 0) + return res < 0; + if(lhs->m_line != rhs->m_line) + return lhs->m_line < rhs->m_line; + return lhs->m_template_id < rhs->m_template_id; + } + + // for sorting tests by suite/file/line + bool suiteOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); + if(res != 0) + return res < 0; + return fileOrderComparator(lhs, rhs); + } + + // for sorting tests by name/suite/file/line + bool nameOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_name, rhs->m_name); + if(res != 0) + return res < 0; + return suiteOrderComparator(lhs, rhs); + } + + // all the registered tests + std::set& getRegisteredTests() { + static std::set data; + return data; + } + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + HANDLE g_stdoutHandle; + WORD g_origFgAttrs; + WORD g_origBgAttrs; + bool g_attrsInitted = false; + + int colors_init() { + if(!g_attrsInitted) { + g_stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + g_attrsInitted = true; + CONSOLE_SCREEN_BUFFER_INFO csbiInfo; + GetConsoleScreenBufferInfo(g_stdoutHandle, &csbiInfo); + g_origFgAttrs = csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | + BACKGROUND_BLUE | BACKGROUND_INTENSITY); + g_origBgAttrs = csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | + FOREGROUND_BLUE | FOREGROUND_INTENSITY); + } + return 0; + } + + int dumy_init_console_colors = colors_init(); +#endif // DOCTEST_CONFIG_COLORS_WINDOWS + + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + void color_to_stream(std::ostream& s, Color::Enum code) { + ((void)s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS + ((void)code); // for DOCTEST_CONFIG_COLORS_NONE +#ifdef DOCTEST_CONFIG_COLORS_ANSI + if(g_no_colors || + (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) + return; + + auto col = ""; + // clang-format off + switch(code) { //!OCLINT missing break in switch statement / unnecessary default statement in covered switch statement + case Color::Red: col = "[0;31m"; break; + case Color::Green: col = "[0;32m"; break; + case Color::Blue: col = "[0;34m"; break; + case Color::Cyan: col = "[0;36m"; break; + case Color::Yellow: col = "[0;33m"; break; + case Color::Grey: col = "[1;30m"; break; + case Color::LightGrey: col = "[0;37m"; break; + case Color::BrightRed: col = "[1;31m"; break; + case Color::BrightGreen: col = "[1;32m"; break; + case Color::BrightWhite: col = "[1;37m"; break; + case Color::Bright: // invalid + case Color::None: + case Color::White: + default: col = "[0m"; + } + // clang-format on + s << "\033" << col; +#endif // DOCTEST_CONFIG_COLORS_ANSI + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + if(g_no_colors || + (isatty(fileno(stdout)) == false && getContextOptions()->force_colors == false)) + return; + +#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(g_stdoutHandle, x | g_origBgAttrs) + + // clang-format off + switch (code) { + case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; + case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; + case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; + case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; + case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; + case Color::Grey: DOCTEST_SET_ATTR(0); break; + case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; + case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; + case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; + case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::None: + case Color::Bright: // invalid + default: DOCTEST_SET_ATTR(g_origFgAttrs); + } + // clang-format on +#endif // DOCTEST_CONFIG_COLORS_WINDOWS + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + + std::vector& getExceptionTranslators() { + static std::vector data; + return data; + } + + String translateActiveException() { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + String res; + auto& translators = getExceptionTranslators(); + for(auto& curr : translators) + if(curr->translate(res)) + return res; + // clang-format off + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") + try { + throw; + } catch(std::exception& ex) { + return ex.what(); + } catch(std::string& msg) { + return msg.c_str(); + } catch(const char* msg) { + return msg; + } catch(...) { + return "unknown exception"; + } + DOCTEST_GCC_SUPPRESS_WARNING_POP +// clang-format on +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + return ""; +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } +} // namespace + +namespace detail { + // used by the macros for registering tests + int regTest(const TestCase& tc) { + getRegisteredTests().insert(tc); + return 0; + } + + // sets the current test suite + int setTestSuite(const TestSuite& ts) { + doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; + return 0; + } + +#ifdef DOCTEST_IS_DEBUGGER_ACTIVE + bool isDebuggerActive() { return DOCTEST_IS_DEBUGGER_ACTIVE(); } +#else // DOCTEST_IS_DEBUGGER_ACTIVE +#ifdef DOCTEST_PLATFORM_MAC + // The following function is taken directly from the following technical note: + // https://developer.apple.com/library/archive/qa/qa1361/_index.html + // Returns true if the current process is being debugged (either + // running under the debugger or has a debugger attached post facto). + bool isDebuggerActive() { + int mib[4]; + kinfo_proc info; + size_t size; + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + // Call sysctl. + size = sizeof(info); + if(sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, 0, 0) != 0) { + std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; + return false; + } + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); + } +#elif DOCTEST_MSVC || defined(__MINGW32__) + bool isDebuggerActive() { return ::IsDebuggerPresent() != 0; } +#else + bool isDebuggerActive() { return false; } +#endif // Platform +#endif // DOCTEST_IS_DEBUGGER_ACTIVE + + void registerExceptionTranslatorImpl(const IExceptionTranslator* et) { + if(std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == + getExceptionTranslators().end()) + getExceptionTranslators().push_back(et); + } + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + void toStream(std::ostream* s, char* in) { *s << in; } + void toStream(std::ostream* s, const char* in) { *s << in; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + void toStream(std::ostream* s, bool in) { *s << std::boolalpha << in << std::noboolalpha; } + void toStream(std::ostream* s, float in) { *s << in; } + void toStream(std::ostream* s, double in) { *s << in; } + void toStream(std::ostream* s, double long in) { *s << in; } + + void toStream(std::ostream* s, char in) { *s << in; } + void toStream(std::ostream* s, char signed in) { *s << in; } + void toStream(std::ostream* s, char unsigned in) { *s << in; } + void toStream(std::ostream* s, int short in) { *s << in; } + void toStream(std::ostream* s, int short unsigned in) { *s << in; } + void toStream(std::ostream* s, int in) { *s << in; } + void toStream(std::ostream* s, int unsigned in) { *s << in; } + void toStream(std::ostream* s, int long in) { *s << in; } + void toStream(std::ostream* s, int long unsigned in) { *s << in; } + void toStream(std::ostream* s, int long long in) { *s << in; } + void toStream(std::ostream* s, int long long unsigned in) { *s << in; } + + DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() + + ContextScopeBase::ContextScopeBase() { + g_infoContexts.push_back(this); + } + + // destroy cannot be inlined into the destructor because that would mean calling stringify after + // ContextScope has been destroyed (base class destructors run after derived class destructors). + // Instead, ContextScope calls this method directly from its destructor. + void ContextScopeBase::destroy() { +#if __cplusplus >= 201703L && defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411 + if(std::uncaught_exceptions() > 0) { +#else + if(std::uncaught_exception()) { +#endif + std::ostringstream s; + this->stringify(&s); + g_cs->stringifiedContexts.push_back(s.str().c_str()); + } + g_infoContexts.pop_back(); + } + +} // namespace detail +namespace { + using namespace detail; + + std::ostream& file_line_to_stream(std::ostream& s, const char* file, int line, + const char* tail = "") { + const auto opt = getContextOptions(); + s << Color::LightGrey << skipPathFromFilename(file) << (opt->gnu_file_line ? ":" : "(") + << (opt->no_line_numbers ? 0 : line) // 0 or the real num depending on the option + << (opt->gnu_file_line ? ":" : "):") << tail; + return s; + } + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) + struct FatalConditionHandler + { + void reset() {} + }; +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + + void reportFatal(const std::string&); + +#ifdef DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + DWORD id; + const char* name; + }; + // There is no 1-1 mapping between signals and windows exceptions. + // Windows can easily distinguish between SO and SigSegV, + // but SigInt, SigTerm, etc are handled differently. + SignalDefs signalDefs[] = { + {EXCEPTION_ILLEGAL_INSTRUCTION, "SIGILL - Illegal instruction signal"}, + {EXCEPTION_STACK_OVERFLOW, "SIGSEGV - Stack overflow"}, + {EXCEPTION_ACCESS_VIOLATION, "SIGSEGV - Segmentation violation signal"}, + {EXCEPTION_INT_DIVIDE_BY_ZERO, "Divide by zero error"}, + }; + + struct FatalConditionHandler + { + static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo) { + for(size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + if(ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { + reportFatal(signalDefs[i].name); + break; + } + } + // If its not an exception we care about, pass it along. + // This stops us from eating debugger breaks etc. + return EXCEPTION_CONTINUE_SEARCH; + } + + FatalConditionHandler() { + isSet = true; + // 32k seems enough for doctest to handle stack overflow, + // but the value was found experimentally, so there is no strong guarantee + guaranteeSize = 32 * 1024; + // Register an unhandled exception filter + previousTop = SetUnhandledExceptionFilter(handleException); + // Pass in guarantee size to be filled + SetThreadStackGuarantee(&guaranteeSize); + } + + static void reset() { + if(isSet) { + // Unregister handler and restore the old guarantee + SetUnhandledExceptionFilter(previousTop); + SetThreadStackGuarantee(&guaranteeSize); + previousTop = nullptr; + isSet = false; + } + } + + ~FatalConditionHandler() { reset(); } + + private: + static bool isSet; + static ULONG guaranteeSize; + static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; + }; + + bool FatalConditionHandler::isSet = false; + ULONG FatalConditionHandler::guaranteeSize = 0; + LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; + +#else // DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + int id; + const char* name; + }; + SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"}, + {SIGILL, "SIGILL - Illegal instruction signal"}, + {SIGFPE, "SIGFPE - Floating point error signal"}, + {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, + {SIGTERM, "SIGTERM - Termination request signal"}, + {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}}; + + struct FatalConditionHandler + { + static bool isSet; + static struct sigaction oldSigActions[DOCTEST_COUNTOF(signalDefs)]; + static stack_t oldSigStack; + static char altStackMem[4 * SIGSTKSZ]; + + static void handleSignal(int sig) { + const char* name = ""; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + SignalDefs& def = signalDefs[i]; + if(sig == def.id) { + name = def.name; + break; + } + } + reset(); + reportFatal(name); + raise(sig); + } + + FatalConditionHandler() { + isSet = true; + stack_t sigStack; + sigStack.ss_sp = altStackMem; + sigStack.ss_size = sizeof(altStackMem); + sigStack.ss_flags = 0; + sigaltstack(&sigStack, &oldSigStack); + struct sigaction sa = {}; + sa.sa_handler = handleSignal; // NOLINT + sa.sa_flags = SA_ONSTACK; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); + } + } + + ~FatalConditionHandler() { reset(); } + static void reset() { + if(isSet) { + // Set signals back to previous values -- hopefully nobody overwrote them in the meantime + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); + } + // Return the old stack + sigaltstack(&oldSigStack, nullptr); + isSet = false; + } + } + }; + + bool FatalConditionHandler::isSet = false; + struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; + stack_t FatalConditionHandler::oldSigStack = {}; + char FatalConditionHandler::altStackMem[] = {}; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace + +namespace { + using namespace detail; + +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) +#else + // TODO: integration with XCode and other IDEs +#define DOCTEST_OUTPUT_DEBUG_STRING(text) // NOLINT(clang-diagnostic-unused-macros) +#endif // Platform + + void addAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsCurrentTest_atomic++; + } + + void addFailedAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsFailedCurrentTest_atomic++; + } + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) + void reportFatal(const std::string& message) { + g_cs->failure_flags |= TestCaseFailureReason::Crash; + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); + + while(g_cs->subcasesStack.size()) { + g_cs->subcasesStack.pop_back(); + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + + g_cs->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH +} // namespace +namespace detail { + + ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const char* exception_string) { + m_test_case = g_cs->currentTest; + m_at = at; + m_file = file; + m_line = line; + m_expr = expr; + m_failed = true; + m_threw = false; + m_threw_as = false; + m_exception_type = exception_type; + m_exception_string = exception_string; +#if DOCTEST_MSVC + if(m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC + ++m_expr; +#endif // MSVC + } + + void ResultBuilder::setResult(const Result& res) { + m_decomp = res.m_decomp; + m_failed = !res.m_passed; + } + + void ResultBuilder::translateException() { + m_threw = true; + m_exception = translateActiveException(); + } + + bool ResultBuilder::log() { + if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw; + } else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT + m_failed = !m_threw_as || (m_exception != m_exception_string); + } else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw_as; + } else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + m_failed = m_exception != m_exception_string; + } else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + m_failed = m_threw; + } + + if(m_exception.size()) + m_exception = String("\"") + m_exception + "\""; + + if(is_running_in_test) { + addAssert(m_at); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); + + if(m_failed) + addFailedAssert(m_at); + } else if(m_failed) { + failed_out_of_a_testing_context(*this); + } + + return m_failed && isDebuggerActive() && + !getContextOptions()->no_breaks; // break into debugger + } + + void ResultBuilder::react() const { + if(m_failed && checkIfShouldThrow(m_at)) + throwException(); + } + + void failed_out_of_a_testing_context(const AssertData& ad) { + if(g_cs->ah) + g_cs->ah(ad); + else + std::abort(); + } + + void decomp_assert(assertType::Enum at, const char* file, int line, const char* expr, + Result result) { + bool failed = !result.m_passed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); + DOCTEST_ASSERT_IN_TESTS(result.m_decomp); + } + + MessageBuilder::MessageBuilder(const char* file, int line, assertType::Enum severity) { + m_stream = getTlsOss(); + m_file = file; + m_line = line; + m_severity = severity; + } + + IExceptionTranslator::IExceptionTranslator() = default; + IExceptionTranslator::~IExceptionTranslator() = default; + + bool MessageBuilder::log() { + m_string = getTlsOssResult(); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); + + const bool isWarn = m_severity & assertType::is_warn; + + // warn is just a message in this context so we don't treat it as an assert + if(!isWarn) { + addAssert(m_severity); + addFailedAssert(m_severity); + } + + return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn; // break + } + + void MessageBuilder::react() { + if(m_severity & assertType::is_require) //!OCLINT bitwise operator in conditional + throwException(); + } + + MessageBuilder::~MessageBuilder() = default; +} // namespace detail +namespace { + using namespace detail; + + template + [[noreturn]] void throw_exception(Ex const& e) { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + throw e; +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + std::cerr << "doctest will terminate because it needed to throw an exception.\n" + << "The message was: " << e.what() << '\n'; + std::terminate(); +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } + +#define DOCTEST_INTERNAL_ERROR(msg) \ + throw_exception(std::logic_error( \ + __FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) + + // clang-format off + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + + class XmlEncode { + public: + enum ForWhat { ForTextNodes, ForAttributes }; + + XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); + + void encodeTo( std::ostream& os ) const; + + friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); + + private: + std::string m_str; + ForWhat m_forWhat; + }; + + class XmlWriter { + public: + + class ScopedElement { + public: + ScopedElement( XmlWriter* writer ); + + ScopedElement( ScopedElement&& other ) noexcept; + ScopedElement& operator=( ScopedElement&& other ) noexcept; + + ~ScopedElement(); + + ScopedElement& writeText( std::string const& text, bool indent = true ); + + template + ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { + m_writer->writeAttribute( name, attribute ); + return *this; + } + + private: + mutable XmlWriter* m_writer = nullptr; + }; + + XmlWriter( std::ostream& os = std::cout ); + ~XmlWriter(); + + XmlWriter( XmlWriter const& ) = delete; + XmlWriter& operator=( XmlWriter const& ) = delete; + + XmlWriter& startElement( std::string const& name ); + + ScopedElement scopedElement( std::string const& name ); + + XmlWriter& endElement(); + + XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); + + XmlWriter& writeAttribute( std::string const& name, const char* attribute ); + + XmlWriter& writeAttribute( std::string const& name, bool attribute ); + + template + XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { + std::stringstream rss; + rss << attribute; + return writeAttribute( name, rss.str() ); + } + + XmlWriter& writeText( std::string const& text, bool indent = true ); + + //XmlWriter& writeComment( std::string const& text ); + + //void writeStylesheetRef( std::string const& url ); + + //XmlWriter& writeBlankLine(); + + void ensureTagClosed(); + + private: + + void writeDeclaration(); + + void newlineIfNecessary(); + + bool m_tagIsOpen = false; + bool m_needsNewline = false; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + +using uchar = unsigned char; + +namespace { + + size_t trailingBytes(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return 2; + } + if ((c & 0xF0) == 0xE0) { + return 3; + } + if ((c & 0xF8) == 0xF0) { + return 4; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + uint32_t headerValue(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return c & 0x1F; + } + if ((c & 0xF0) == 0xE0) { + return c & 0x0F; + } + if ((c & 0xF8) == 0xF0) { + return c & 0x07; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + void hexEscapeChar(std::ostream& os, unsigned char c) { + std::ios_base::fmtflags f(os.flags()); + os << "\\x" + << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(c); + os.flags(f); + } + +} // anonymous namespace + + XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) + : m_str( str ), + m_forWhat( forWhat ) + {} + + void XmlEncode::encodeTo( std::ostream& os ) const { + // Apostrophe escaping not necessary if we always use " to write attributes + // (see: https://www.w3.org/TR/xml/#syntax) + + for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { + uchar c = m_str[idx]; + switch (c) { + case '<': os << "<"; break; + case '&': os << "&"; break; + + case '>': + // See: https://www.w3.org/TR/xml/#syntax + if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') + os << ">"; + else + os << c; + break; + + case '\"': + if (m_forWhat == ForAttributes) + os << """; + else + os << c; + break; + + default: + // Check for control characters and invalid utf-8 + + // Escape control characters in standard ascii + // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { + hexEscapeChar(os, c); + break; + } + + // Plain ASCII: Write it to stream + if (c < 0x7F) { + os << c; + break; + } + + // UTF-8 territory + // Check if the encoding is valid and if it is not, hex escape bytes. + // Important: We do not check the exact decoded values for validity, only the encoding format + // First check that this bytes is a valid lead byte: + // This means that it is not encoded as 1111 1XXX + // Or as 10XX XXXX + if (c < 0xC0 || + c >= 0xF8) { + hexEscapeChar(os, c); + break; + } + + auto encBytes = trailingBytes(c); + // Are there enough bytes left to avoid accessing out-of-bounds memory? + if (idx + encBytes - 1 >= m_str.size()) { + hexEscapeChar(os, c); + break; + } + // The header is valid, check data + // The next encBytes bytes must together be a valid utf-8 + // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + bool valid = true; + uint32_t value = headerValue(c); + for (std::size_t n = 1; n < encBytes; ++n) { + uchar nc = m_str[idx + n]; + valid &= ((nc & 0xC0) == 0x80); + value = (value << 6) | (nc & 0x3F); + } + + if ( + // Wrong bit pattern of following bytes + (!valid) || + // Overlong encodings + (value < 0x80) || + ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant + (0x800 < value && value < 0x10000 && encBytes > 3) || + // Encoded value out of range + (value >= 0x110000) + ) { + hexEscapeChar(os, c); + break; + } + + // If we got here, this is in fact a valid(ish) utf-8 sequence + for (std::size_t n = 0; n < encBytes; ++n) { + os << m_str[idx + n]; + } + idx += encBytes - 1; + break; + } + } + } + + std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { + xmlEncode.encodeTo( os ); + return os; + } + + XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + {} + + XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) noexcept + : m_writer( other.m_writer ){ + other.m_writer = nullptr; + } + XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) noexcept { + if ( m_writer ) { + m_writer->endElement(); + } + m_writer = other.m_writer; + other.m_writer = nullptr; + return *this; + } + + + XmlWriter::ScopedElement::~ScopedElement() { + if( m_writer ) + m_writer->endElement(); + } + + XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { + m_writer->writeText( text, indent ); + return *this; + } + + XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) + { + writeDeclaration(); + } + + XmlWriter::~XmlWriter() { + while( !m_tags.empty() ) + endElement(); + } + + XmlWriter& XmlWriter::startElement( std::string const& name ) { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << '<' << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& XmlWriter::endElement() { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) { + m_os << "/>"; + m_tagIsOpen = false; + } + else { + m_os << m_indent << ""; + } + m_os << std::endl; + m_tags.pop_back(); + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { + if( !name.empty() && !attribute.empty() ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { + if( !name.empty() && attribute && attribute[0] != '\0' ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { + m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { + if( !text.empty() ){ + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen && indent ) + m_os << m_indent; + m_os << XmlEncode( text ); + m_needsNewline = true; + } + return *this; + } + + //XmlWriter& XmlWriter::writeComment( std::string const& text ) { + // ensureTagClosed(); + // m_os << m_indent << ""; + // m_needsNewline = true; + // 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::endl; + m_tagIsOpen = false; + } + } + + void XmlWriter::writeDeclaration() { + m_os << "\n"; + } + + void XmlWriter::newlineIfNecessary() { + if( m_needsNewline ) { + m_os << std::endl; + m_needsNewline = false; + } + } + +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + + // clang-format on + + struct XmlReporter : public IReporter + { + XmlWriter xml; + std::mutex mutex; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + XmlReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + std::stringstream ss; + for(int i = 0; i < num_contexts; ++i) { + contexts[i]->stringify(&ss); + xml.scopedElement("Info").writeText(ss.str()); + ss.str(""); + } + } + } + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + void test_case_start_impl(const TestCaseData& in) { + bool open_ts_tag = false; + if(tc != nullptr) { // we have already opened a test suite + if(std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { + xml.endElement(); + open_ts_tag = true; + } + } + else { + open_ts_tag = true; // first test case ==> first test suite + } + + if(open_ts_tag) { + xml.startElement("TestSuite"); + xml.writeAttribute("name", in.m_test_suite); + } + + tc = ∈ + xml.startElement("TestCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)) + .writeAttribute("description", in.m_description); + + if(Approx(in.m_timeout) != 0) + xml.writeAttribute("timeout", in.m_timeout); + if(in.m_may_fail) + xml.writeAttribute("may_fail", true); + if(in.m_should_fail) + xml.writeAttribute("should_fail", true); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + test_run_start(); + if(opt.list_reporters) { + for(auto& curr : getListeners()) + xml.scopedElement("Listener") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + for(auto& curr : getReporters()) + xml.scopedElement("Reporter") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + } else if(opt.count || opt.list_test_cases) { + for(unsigned i = 0; i < in.num_data; ++i) { + xml.scopedElement("TestCase").writeAttribute("name", in.data[i]->m_name) + .writeAttribute("testsuite", in.data[i]->m_test_suite) + .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file)) + .writeAttribute("line", line(in.data[i]->m_line)); + } + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + } else if(opt.list_test_suites) { + for(unsigned i = 0; i < in.num_data; ++i) + xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + xml.scopedElement("OverallResultsTestSuites") + .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); + } + xml.endElement(); + } + + void test_run_start() override { + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("doctest").writeAttribute("binary", binary_name); + if(opt.no_version == false) + xml.writeAttribute("version", DOCTEST_VERSION_STR); + + // only the consequential ones (TODO: filters) + xml.scopedElement("Options") + .writeAttribute("order_by", opt.order_by.c_str()) + .writeAttribute("rand_seed", opt.rand_seed) + .writeAttribute("first", opt.first) + .writeAttribute("last", opt.last) + .writeAttribute("abort_after", opt.abort_after) + .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) + .writeAttribute("case_sensitive", opt.case_sensitive) + .writeAttribute("no_throw", opt.no_throw) + .writeAttribute("no_skip", opt.no_skip); + } + + void test_run_end(const TestRunStats& p) override { + if(tc) // the TestSuite tag - only if there has been at least 1 test case + xml.endElement(); + + xml.scopedElement("OverallResultsAsserts") + .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) + .writeAttribute("failures", p.numAssertsFailed); + + xml.startElement("OverallResultsTestCases") + .writeAttribute("successes", + p.numTestCasesPassingFilters - p.numTestCasesFailed) + .writeAttribute("failures", p.numTestCasesFailed); + if(opt.no_skipped_summary == false) + xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); + xml.endElement(); + + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + test_case_start_impl(in); + xml.ensureTagClosed(); + } + + void test_case_reenter(const TestCaseData&) override {} + + void test_case_end(const CurrentTestCaseStats& st) override { + xml.startElement("OverallResultsAsserts") + .writeAttribute("successes", + st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) + .writeAttribute("failures", st.numAssertsFailedCurrentTest); + if(opt.duration) + xml.writeAttribute("duration", st.seconds); + if(tc->m_expected_failures) + xml.writeAttribute("expected_failures", tc->m_expected_failures); + xml.endElement(); + + xml.endElement(); + } + + void test_case_exception(const TestCaseException& e) override { + std::lock_guard lock(mutex); + + xml.scopedElement("Exception") + .writeAttribute("crash", e.is_crash) + .writeText(e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + std::lock_guard lock(mutex); + + xml.startElement("SubCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)); + xml.ensureTagClosed(); + } + + void subcase_end() override { xml.endElement(); } + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed && !opt.success) + return; + + std::lock_guard lock(mutex); + + xml.startElement("Expression") + .writeAttribute("success", !rb.m_failed) + .writeAttribute("type", assertString(rb.m_at)) + .writeAttribute("filename", skipPathFromFilename(rb.m_file)) + .writeAttribute("line", line(rb.m_line)); + + xml.scopedElement("Original").writeText(rb.m_expr); + + if(rb.m_threw) + xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); + + if(rb.m_at & assertType::is_throws_as) + xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); + if(rb.m_at & assertType::is_throws_with) + xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string); + if((rb.m_at & assertType::is_normal) && !rb.m_threw) + xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void log_message(const MessageData& mb) override { + std::lock_guard lock(mutex); + + xml.startElement("Message") + .writeAttribute("type", failureString(mb.m_severity)) + .writeAttribute("filename", skipPathFromFilename(mb.m_file)) + .writeAttribute("line", line(mb.m_line)); + + xml.scopedElement("Text").writeText(mb.m_string.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void test_case_skipped(const TestCaseData& in) override { + if(opt.no_skipped_summary == false) { + test_case_start_impl(in); + xml.writeAttribute("skipped", "true"); + xml.endElement(); + } + } + }; + + DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); + + struct Whitespace + { + int nrSpaces; + explicit Whitespace(int nr) + : nrSpaces(nr) {} + }; + + std::ostream& operator<<(std::ostream& out, const Whitespace& ws) { + if(ws.nrSpaces != 0) + out << std::setw(ws.nrSpaces) << ' '; + return out; + } + + struct ConsoleReporter : public IReporter + { + std::ostream& s; + bool hasLoggedCurrentTestStart; + std::vector subcasesStack; + std::mutex mutex; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc; + + ConsoleReporter(const ContextOptions& co) + : s(*co.cout) + , opt(co) {} + + ConsoleReporter(const ContextOptions& co, std::ostream& ostr) + : s(ostr) + , opt(co) {} + + // ========================================================================================= + // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE + // ========================================================================================= + + void separator_to_stream() { + s << Color::Yellow + << "===============================================================================" + "\n"; + } + + const char* getSuccessOrFailString(bool success, assertType::Enum at, + const char* success_str) { + if(success) + return success_str; + return failureString(at); + } + + Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at) { + return success ? Color::BrightGreen : + (at & assertType::is_warn) ? Color::Yellow : Color::Red; + } + + void successOrFailColoredStringToStream(bool success, assertType::Enum at, + const char* success_str = "SUCCESS") { + s << getSuccessOrFailColor(success, at) + << getSuccessOrFailString(success, at, success_str) << ": "; + } + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << Color::None << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << "\n"; + } + } + + s << "\n"; + } + + void logTestStart() { + if(hasLoggedCurrentTestStart) + return; + + separator_to_stream(); + file_line_to_stream(s, tc->m_file, tc->m_line, "\n"); + if(tc->m_description) + s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; + if(tc->m_test_suite && tc->m_test_suite[0] != '\0') + s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; + if(strncmp(tc->m_name, " Scenario:", 11) != 0) + s << Color::None << "TEST CASE: "; + s << Color::None << tc->m_name << "\n"; + + for(auto& curr : subcasesStack) + if(curr.m_name[0] != '\0') + s << " " << curr.m_name << "\n"; + + s << "\n"; + + hasLoggedCurrentTestStart = true; + } + + void printVersion() { + if(opt.no_version == false) + s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" + << DOCTEST_VERSION_STR << "\"\n"; + } + + void printIntro() { + printVersion(); + s << Color::Cyan << "[doctest] " << Color::None + << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; + } + + void printHelp() { + int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); + printVersion(); + // clang-format off + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filters use wildcards for matching strings\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "something passes a filter if any of the strings in a filter matches\n"; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; +#endif + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "Query flags - the program quits after them. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " + << Whitespace(sizePrefixDisplay*0) << "prints this message\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " + << Whitespace(sizePrefixDisplay*1) << "prints the version\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " + << Whitespace(sizePrefixDisplay*1) << "prints the number of matching tests\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " + << Whitespace(sizePrefixDisplay*1) << "lists all matching tests by name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " + << Whitespace(sizePrefixDisplay*1) << "lists all matching test suites\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " + << Whitespace(sizePrefixDisplay*1) << "lists all registered reporters\n\n"; + // ================================================================================== << 79 + s << Color::Cyan << "[doctest] " << Color::None; + s << "The available / options/filters are:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " + << Whitespace(sizePrefixDisplay*1) << "filters subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " + << Whitespace(sizePrefixDisplay*1) << "reporters to use (console is default)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " + << Whitespace(sizePrefixDisplay*1) << "output filename\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " + << Whitespace(sizePrefixDisplay*1) << "how the tests should be ordered\n"; + s << Whitespace(sizePrefixDisplay*3) << " - by [file/suite/name/rand]\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " + << Whitespace(sizePrefixDisplay*1) << "seed for random ordering\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " + << Whitespace(sizePrefixDisplay*1) << "the first test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " + << Whitespace(sizePrefixDisplay*1) << "the last test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " + << Whitespace(sizePrefixDisplay*1) << "stop after failed assertions\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " + << Whitespace(sizePrefixDisplay*1) << "apply filters for the first levels\n"; + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " + << Whitespace(sizePrefixDisplay*1) << "include successful assertions in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " + << Whitespace(sizePrefixDisplay*1) << "filters being treated as case sensitive\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " + << Whitespace(sizePrefixDisplay*1) << "exits after the tests finish\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " + << Whitespace(sizePrefixDisplay*1) << "prints the time duration of each test\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " + << Whitespace(sizePrefixDisplay*1) << "skips exceptions-related assert checks\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " + << Whitespace(sizePrefixDisplay*1) << "returns (or exits) always with success\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " + << Whitespace(sizePrefixDisplay*1) << "skips all runtime doctest operations\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework version in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " + << Whitespace(sizePrefixDisplay*1) << "disables colors in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " + << Whitespace(sizePrefixDisplay*1) << "use colors even when not in a tty\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " + << Whitespace(sizePrefixDisplay*1) << "disables breakpoints in debuggers\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " + << Whitespace(sizePrefixDisplay*1) << "don't skip test cases marked as skip\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " + << Whitespace(sizePrefixDisplay*1) << ":n: vs (n): for line numbers in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " + << Whitespace(sizePrefixDisplay*1) << "only filenames and no paths in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " + << Whitespace(sizePrefixDisplay*1) << "0 instead of real line numbers in output\n"; + // ================================================================================== << 79 + // clang-format on + + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "for more information visit the project documentation\n\n"; + } + + void printRegisteredReporters() { + printVersion(); + auto printReporters = [this] (const reporterMap& reporters, const char* type) { + if(reporters.size()) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; + for(auto& curr : reporters) + s << "priority: " << std::setw(5) << curr.first.first + << " name: " << curr.first.second << "\n"; + } + }; + printReporters(getListeners(), "listeners"); + printReporters(getReporters(), "reporters"); + } + + void list_query_results() { + separator_to_stream(); + if(opt.count || opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + } else if(opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " + << g_cs->numTestSuitesPassingFilters << "\n"; + } + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + if(opt.version) { + printVersion(); + } else if(opt.help) { + printHelp(); + } else if(opt.list_reporters) { + printRegisteredReporters(); + } else if(opt.count || opt.list_test_cases) { + if(opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None + << "listing all test case names\n"; + separator_to_stream(); + } + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_name << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + + } else if(opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; + separator_to_stream(); + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_test_suite << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " + << g_cs->numTestSuitesPassingFilters << "\n"; + } + } + + void test_run_start() override { printIntro(); } + + void test_run_end(const TestRunStats& p) override { + separator_to_stream(); + s << std::dec; + + const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; + s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(6) + << p.numTestCasesPassingFilters << " | " + << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : + Color::Green) + << std::setw(6) << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" + << Color::None << " | " << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) + << std::setw(6) << p.numTestCasesFailed << " failed" << Color::None << " | "; + if(opt.no_skipped_summary == false) { + const int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; + s << (numSkipped == 0 ? Color::None : Color::Yellow) << std::setw(6) << numSkipped + << " skipped" << Color::None; + } + s << "\n"; + s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(6) + << p.numAsserts << " | " + << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) + << std::setw(6) << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None + << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(6) + << p.numAssertsFailed << " failed" << Color::None << " |\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) + << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; + } + + void test_case_start(const TestCaseData& in) override { + hasLoggedCurrentTestStart = false; + tc = ∈ + } + + void test_case_reenter(const TestCaseData&) override {} + + void test_case_end(const CurrentTestCaseStats& st) override { + // log the preamble of the test case only if there is something + // else to print - something other than that an assert has failed + if(opt.duration || + (st.failure_flags && st.failure_flags != TestCaseFailureReason::AssertFailure)) + logTestStart(); + + if(opt.duration) + s << Color::None << std::setprecision(6) << std::fixed << st.seconds + << " s: " << tc->m_name << "\n"; + + if(st.failure_flags & TestCaseFailureReason::Timeout) + s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) + << std::fixed << tc->m_timeout << "!\n"; + + if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { + s << Color::Yellow << "Failed as expected so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { + s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures + << " times so marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { + s << Color::Yellow << "Failed exactly " << tc->m_expected_failures + << " times as expected so marking it as not failed!\n"; + } + if(st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + s << Color::Red << "Aborting - too many failed asserts!\n"; + } + s << Color::None; // lgtm [cpp/useless-expression] + } + + void test_case_exception(const TestCaseException& e) override { + logTestStart(); + + file_line_to_stream(s, tc->m_file, tc->m_line, " "); + successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : + assertType::is_check); + s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ") + << Color::Cyan << e.error_string << "\n"; + + int num_stringified_contexts = get_num_stringified_contexts(); + if(num_stringified_contexts) { + auto stringified_contexts = get_stringified_contexts(); + s << Color::None << " logged: "; + for(int i = num_stringified_contexts; i > 0; --i) { + s << (i == num_stringified_contexts ? "" : " ") + << stringified_contexts[i - 1] << "\n"; + } + } + s << "\n" << Color::None; + } + + void subcase_start(const SubcaseSignature& subc) override { + std::lock_guard lock(mutex); + subcasesStack.push_back(subc); + hasLoggedCurrentTestStart = false; + } + + void subcase_end() override { + std::lock_guard lock(mutex); + subcasesStack.pop_back(); + hasLoggedCurrentTestStart = false; + } + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed && !opt.success) + return; + + std::lock_guard lock(mutex); + + logTestStart(); + + file_line_to_stream(s, rb.m_file, rb.m_line, " "); + successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); + if((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == + 0) //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " + << Color::None; + + if(rb.m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; + } else if((rb.m_at & assertType::is_throws_as) && + (rb.m_at & assertType::is_throws_with)) { //!OCLINT + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string << "\", " << rb.m_exception_type << " ) " << Color::None; + if(rb.m_threw) { + if(!rb.m_failed) { + s << "threw as expected!\n"; + } else { + s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; + } + } else { + s << "did NOT throw at all!\n"; + } + } else if(rb.m_at & + assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", " + << rb.m_exception_type << " ) " << Color::None + << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & + assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string << "\" ) " << Color::None + << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan + << rb.m_exception << "\n"; + } else { + s << (rb.m_threw ? "THREW exception: " : + (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); + if(rb.m_threw) + s << rb.m_exception << "\n"; + else + s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; + } + + log_contexts(); + } + + void log_message(const MessageData& mb) override { + std::lock_guard lock(mutex); + + logTestStart(); + + file_line_to_stream(s, mb.m_file, mb.m_line, " "); + s << getSuccessOrFailColor(false, mb.m_severity) + << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, + "MESSAGE") << ": "; + s << Color::None << mb.m_string << "\n"; + log_contexts(); + } + + void test_case_skipped(const TestCaseData&) override {} + }; + + DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); + +#ifdef DOCTEST_PLATFORM_WINDOWS + struct DebugOutputWindowReporter : public ConsoleReporter + { + DOCTEST_THREAD_LOCAL static std::ostringstream oss; + + DebugOutputWindowReporter(const ContextOptions& co) + : ConsoleReporter(co, oss) {} + +#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ + void func(type arg) override { \ + bool with_col = g_no_colors; \ + g_no_colors = false; \ + ConsoleReporter::func(arg); \ + DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ + oss.str(""); \ + g_no_colors = with_col; \ + } + + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData&, in) + }; + + DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; +#endif // DOCTEST_PLATFORM_WINDOWS + + // the implementation of parseOption() + bool parseOptionImpl(int argc, const char* const* argv, const char* pattern, String* value) { + // going from the end to the beginning and stopping on the first occurrence from the end + for(int i = argc; i > 0; --i) { + auto index = i - 1; + auto temp = std::strstr(argv[index], pattern); + if(temp && (value || strlen(temp) == strlen(pattern))) { //!OCLINT prefer early exits and continue + // eliminate matches in which the chars before the option are not '-' + bool noBadCharsFound = true; + auto curr = argv[index]; + while(curr != temp) { + if(*curr++ != '-') { + noBadCharsFound = false; + break; + } + } + if(noBadCharsFound && argv[index][0] == '-') { + if(value) { + // parsing the value of an option + temp += strlen(pattern); + const unsigned len = strlen(temp); + if(len) { + *value = temp; + return true; + } + } else { + // just a flag - no value + return true; + } + } + } + } + return false; + } + + // parses an option and returns the string after the '=' character + bool parseOption(int argc, const char* const* argv, const char* pattern, String* value = nullptr, + const String& defaultVal = String()) { + if(value) + *value = defaultVal; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + // offset (normally 3 for "dt-") to skip prefix + if(parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) + return true; +#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + return parseOptionImpl(argc, argv, pattern, value); + } + + // locates a flag on the command line + bool parseFlag(int argc, const char* const* argv, const char* pattern) { + return parseOption(argc, argv, pattern); + } + + // parses a comma separated list of words after a pattern in one of the arguments in argv + bool parseCommaSepArgs(int argc, const char* const* argv, const char* pattern, + std::vector& res) { + String filtersString; + if(parseOption(argc, argv, pattern, &filtersString)) { + // tokenize with "," as a separator + // cppcheck-suppress strtokCalled + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + auto pch = std::strtok(filtersString.c_str(), ","); // modifies the string + while(pch != nullptr) { + if(strlen(pch)) + res.push_back(pch); + // uses the strtok() internal state to go to the next token + // cppcheck-suppress strtokCalled + pch = std::strtok(nullptr, ","); + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + return true; + } + return false; + } + + enum optionType + { + option_bool, + option_int + }; + + // parses an int/bool option from the command line + bool parseIntOption(int argc, const char* const* argv, const char* pattern, optionType type, + int& res) { + String parsedValue; + if(!parseOption(argc, argv, pattern, &parsedValue)) + return false; + + if(type == 0) { + // boolean + const char positive[][5] = {"1", "true", "on", "yes"}; // 5 - strlen("true") + 1 + const char negative[][6] = {"0", "false", "off", "no"}; // 6 - strlen("false") + 1 + + // if the value matches any of the positive/negative possibilities + for(unsigned i = 0; i < 4; i++) { + if(parsedValue.compare(positive[i], true) == 0) { + res = 1; //!OCLINT parameter reassignment + return true; + } + if(parsedValue.compare(negative[i], true) == 0) { + res = 0; //!OCLINT parameter reassignment + return true; + } + } + } else { + // integer + // TODO: change this to use std::stoi or something else! currently it uses undefined behavior - assumes '0' on failed parse... + int theInt = std::atoi(parsedValue.c_str()); // NOLINT + if(theInt != 0) { + res = theInt; //!OCLINT parameter reassignment + return true; + } + } + return false; + } +} // namespace + +Context::Context(int argc, const char* const* argv) + : p(new detail::ContextState) { + parseArgs(argc, argv, true); + if(argc) + p->binary_name = argv[0]; +} + +Context::~Context() { + if(g_cs == p) + g_cs = nullptr; + delete p; +} + +void Context::applyCommandLine(int argc, const char* const* argv) { + parseArgs(argc, argv); + if(argc) + p->binary_name = argv[0]; +} + +// parses args +void Context::parseArgs(int argc, const char* const* argv, bool withDefaults) { + using namespace detail; + + // clang-format off + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); + // clang-format on + + int intRes = 0; + String strRes; + +#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ + p->var = !!intRes; \ + else if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname)) \ + p->var = true; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ + p->var = intRes; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ + if(parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ + parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || \ + withDefaults) \ + p->var = strRes + + // clang-format off + DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); + DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); + DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); + + DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); + DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); + + DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); + DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); + + DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); + // clang-format on + + if(withDefaults) { + p->help = false; + p->version = false; + p->count = false; + p->list_test_cases = false; + p->list_test_suites = false; + p->list_reporters = false; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { + p->help = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { + p->version = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { + p->count = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { + p->list_test_cases = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { + p->list_test_suites = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { + p->list_reporters = true; + p->exit = true; + } +} + +// allows the user to add procedurally to the filters from the command line +void Context::addFilter(const char* filter, const char* value) { setOption(filter, value); } + +// allows the user to clear all filters from the command line +void Context::clearFilters() { + for(auto& curr : p->filters) + curr.clear(); +} + +// allows the user to override procedurally the int/bool options from the command line +void Context::setOption(const char* option, int value) { + setOption(option, toString(value).c_str()); +} + +// allows the user to override procedurally the string options from the command line +void Context::setOption(const char* option, const char* value) { + auto argv = String("-") + option + "=" + value; + auto lvalue = argv.c_str(); + parseArgs(1, &lvalue); +} + +// users should query this in their main() and exit the program if true +bool Context::shouldExit() { return p->exit; } + +void Context::setAsDefaultForAssertsOutOfTestCases() { g_cs = p; } + +void Context::setAssertHandler(detail::assert_handler ah) { p->ah = ah; } + +// the main function that does all the filtering and test running +int Context::run() { + using namespace detail; + + // save the old context state in case such was setup - for using asserts out of a testing context + auto old_cs = g_cs; + // this is the current contest + g_cs = p; + is_running_in_test = true; + + g_no_colors = p->no_colors; + p->resetRunData(); + + // stdout by default + p->cout = &std::cout; + p->cerr = &std::cerr; + + // or to a file if specified + std::fstream fstr; + if(p->out.size()) { + fstr.open(p->out.c_str(), std::fstream::out); + p->cout = &fstr; + } + + auto cleanup_and_return = [&]() { + if(fstr.is_open()) + fstr.close(); + + // restore context + g_cs = old_cs; + is_running_in_test = false; + + // we have to free the reporters which were allocated when the run started + for(auto& curr : p->reporters_currently_used) + delete curr; + p->reporters_currently_used.clear(); + + if(p->numTestCasesFailed && !p->no_exitcode) + return EXIT_FAILURE; + return EXIT_SUCCESS; + }; + + // setup default reporter if none is given through the command line + if(p->filters[8].empty()) + p->filters[8].push_back("console"); + + // check to see if any of the registered reporters has been selected + for(auto& curr : getReporters()) { + if(matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) + p->reporters_currently_used.push_back(curr.second(*g_cs)); + } + + // TODO: check if there is nothing in reporters_currently_used + + // prepend all listeners + for(auto& curr : getListeners()) + p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); + +#ifdef DOCTEST_PLATFORM_WINDOWS + if(isDebuggerActive()) + p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); +#endif // DOCTEST_PLATFORM_WINDOWS + + // handle version, help and no_run + if(p->no_run || p->version || p->help || p->list_reporters) { + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); + + return cleanup_and_return(); + } + + std::vector testArray; + for(auto& curr : getRegisteredTests()) + testArray.push_back(&curr); + p->numTestCases = testArray.size(); + + // sort the collected records + if(!testArray.empty()) { + if(p->order_by.compare("file", true) == 0) { + std::sort(testArray.begin(), testArray.end(), fileOrderComparator); + } else if(p->order_by.compare("suite", true) == 0) { + std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); + } else if(p->order_by.compare("name", true) == 0) { + std::sort(testArray.begin(), testArray.end(), nameOrderComparator); + } else if(p->order_by.compare("rand", true) == 0) { + std::srand(p->rand_seed); + + // random_shuffle implementation + const auto first = &testArray[0]; + for(size_t i = testArray.size() - 1; i > 0; --i) { + int idxToSwap = std::rand() % (i + 1); // NOLINT + + const auto temp = first[i]; + + first[i] = first[idxToSwap]; + first[idxToSwap] = temp; + } + } + } + + std::set testSuitesPassingFilt; + + bool query_mode = p->count || p->list_test_cases || p->list_test_suites; + std::vector queryResults; + + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); + + // invoke the registered functions if they match the filter criteria (or just count them) + for(auto& curr : testArray) { + const auto& tc = *curr; + + bool skip_me = false; + if(tc.m_skip && !p->no_skip) + skip_me = true; + + if(!matchesAny(tc.m_file, p->filters[0], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_file, p->filters[1], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) + skip_me = true; + + if(!skip_me) + p->numTestCasesPassingFilters++; + + // skip the test if it is not in the execution range + if((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || + (p->first > p->numTestCasesPassingFilters)) + skip_me = true; + + if(skip_me) { + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); + continue; + } + + // do not execute the test if we are to only count the number of filter passing tests + if(p->count) + continue; + + // print the name of the test and don't execute it + if(p->list_test_cases) { + queryResults.push_back(&tc); + continue; + } + + // print the name of the test suite if not done already and don't execute it + if(p->list_test_suites) { + if((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { + queryResults.push_back(&tc); + testSuitesPassingFilt.insert(tc.m_test_suite); + p->numTestSuitesPassingFilters++; + } + continue; + } + + // execute the test if it passes all the filtering + { + p->currentTest = &tc; + + p->failure_flags = TestCaseFailureReason::None; + p->seconds = 0; + + // reset atomic counters + p->numAssertsFailedCurrentTest_atomic = 0; + p->numAssertsCurrentTest_atomic = 0; + + p->subcasesPassed.clear(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); + + p->timer.start(); + + bool run_test = true; + + do { + // reset some of the fields for subcases (except for the set of fully passed ones) + p->should_reenter = false; + p->subcasesCurrentMaxLevel = 0; + p->subcasesStack.clear(); + + p->shouldLogCurrentException = true; + + // reset stuff for logging with INFO() + p->stringifiedContexts.clear(); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + FatalConditionHandler fatalConditionHandler; // Handle signals + // execute the test + tc.m_test(); + fatalConditionHandler.reset(); +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + } catch(const TestFailureException&) { + p->failure_flags |= TestCaseFailureReason::AssertFailure; + } catch(...) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, + {translateActiveException(), false}); + p->failure_flags |= TestCaseFailureReason::Exception; + } +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + + // exit this loop if enough assertions have failed - even if there are more subcases + if(p->abort_after > 0 && + p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { + run_test = false; + p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; + } + + if(p->should_reenter && run_test) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); + if(!p->should_reenter) + run_test = false; + } while(run_test); + + p->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + p->currentTest = nullptr; + + // stop executing tests if enough assertions have failed + if(p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) + break; + } + } + + if(!query_mode) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } else { + QueryData qdata; + qdata.run_stats = g_cs; + qdata.data = queryResults.data(); + qdata.num_data = unsigned(queryResults.size()); + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); + } + + // see these issues on the reasoning for this: + // - https://github.com/onqtam/doctest/issues/143#issuecomment-414418903 + // - https://github.com/onqtam/doctest/issues/126 + auto DOCTEST_FIX_FOR_MACOS_LIBCPP_IOSFWD_STRING_LINK_ERRORS = []() DOCTEST_NOINLINE + { std::cout << std::string(); }; + DOCTEST_FIX_FOR_MACOS_LIBCPP_IOSFWD_STRING_LINK_ERRORS(); + + return cleanup_and_return(); +} + +IReporter::~IReporter() = default; + +int IReporter::get_num_active_contexts() { return detail::g_infoContexts.size(); } +const IContextScope* const* IReporter::get_active_contexts() { + return get_num_active_contexts() ? &detail::g_infoContexts[0] : nullptr; +} + +int IReporter::get_num_stringified_contexts() { return detail::g_cs->stringifiedContexts.size(); } +const String* IReporter::get_stringified_contexts() { + return get_num_stringified_contexts() ? &detail::g_cs->stringifiedContexts[0] : nullptr; +} + +namespace detail { + void registerReporterImpl(const char* name, int priority, reporterCreatorFunc c, bool isReporter) { + if(isReporter) + getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + else + getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + } +} // namespace detail + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 +int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_LIBRARY_IMPLEMENTATION +#endif // DOCTEST_CONFIG_IMPLEMENT diff --git a/Firmware/doctest/parts/doctest.cpp b/Firmware/doctest/parts/doctest.cpp new file mode 100644 index 00000000..a423d0a7 --- /dev/null +++ b/Firmware/doctest/parts/doctest.cpp @@ -0,0 +1,3344 @@ +#if defined(DOCTEST_CONFIG_IMPLEMENT) || !defined(DOCTEST_SINGLE_HEADER) + +#ifndef DOCTEST_SINGLE_HEADER +#include "doctest_fwd.h" +#endif // DOCTEST_SINGLE_HEADER + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") + +#ifndef DOCTEST_LIBRARY_IMPLEMENTATION +#define DOCTEST_LIBRARY_IMPLEMENTATION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-local-typedef") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-local-typedefs") +DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4616) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4619) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4996) // The compiler encountered a deprecated declaration +DOCTEST_MSVC_SUPPRESS_WARNING(4267) // 'var' : conversion from 'x' to 'y', possible loss of data +DOCTEST_MSVC_SUPPRESS_WARNING(4706) // assignment within conditional expression +DOCTEST_MSVC_SUPPRESS_WARNING(4512) // 'class' : assignment operator could not be generated +DOCTEST_MSVC_SUPPRESS_WARNING(4127) // conditional expression is constant +DOCTEST_MSVC_SUPPRESS_WARNING(4530) // C++ exception handler used, but unwind semantics not enabled +DOCTEST_MSVC_SUPPRESS_WARNING(4577) // 'noexcept' used with no exception handling mode specified +DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string expected in argument is not a string literal +DOCTEST_MSVC_SUPPRESS_WARNING(4365) // conversion from 'int' to 'unsigned', signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4820) // padding in structs +DOCTEST_MSVC_SUPPRESS_WARNING(4640) // construction of local static object is not thread-safe +DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to potentially throwing function passed to extern C +DOCTEST_MSVC_SUPPRESS_WARNING(5045) // Spectre mitigation stuff +DOCTEST_MSVC_SUPPRESS_WARNING(4626) // assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5027) // move assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5026) // move constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4625) // copy constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4800) // forcing value to bool 'true' or 'false' (performance warning) +// static analysis +DOCTEST_MSVC_SUPPRESS_WARNING(26439) // This kind of function may not throw. Declare it 'noexcept' +DOCTEST_MSVC_SUPPRESS_WARNING(26495) // Always initialize a member variable +DOCTEST_MSVC_SUPPRESS_WARNING(26451) // Arithmetic overflow ... +DOCTEST_MSVC_SUPPRESS_WARNING(26444) // Avoid unnamed objects with custom construction and dtor... +DOCTEST_MSVC_SUPPRESS_WARNING(26812) // Prefer 'enum class' over 'enum' + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN + +// required includes - will go only in one translation unit! +#include +#include +#include +// borland (Embarcadero) compiler requires math.h and not cmath - https://github.com/onqtam/doctest/pull/37 +#ifdef __BORLANDC__ +#include +#endif // __BORLANDC__ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DOCTEST_CONFIG_POSIX_SIGNALS +#include +#endif // DOCTEST_CONFIG_POSIX_SIGNALS +#include +#include +#include + +#ifdef DOCTEST_PLATFORM_MAC +#include +#include +#include +#endif // DOCTEST_PLATFORM_MAC + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// defines for a leaner windows.h +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif // WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX + +// not sure what AfxWin.h is for - here I do what Catch does +#ifdef __AFXDLL +#include +#else +#include +#endif +#include + +#else // DOCTEST_PLATFORM_WINDOWS + +#include +#include + +#endif // DOCTEST_PLATFORM_WINDOWS + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END + +// counts the number of elements in a C array +#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) + +#ifdef DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled +#else // DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled +#endif // DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX +#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" +#endif + +#ifndef DOCTEST_THREAD_LOCAL +#define DOCTEST_THREAD_LOCAL thread_local +#endif + +#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS +#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX +#else +#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" +#endif + +namespace doctest { + +bool is_running_in_test = false; + +namespace { + using namespace detail; + // case insensitive strcmp + int stricmp(const char* a, const char* b) { + for(;; a++, b++) { + const int d = tolower(*a) - tolower(*b); + if(d != 0 || !*a) + return d; + } + } + + template + String fpToString(T value, int precision) { + std::ostringstream oss; + oss << std::setprecision(precision) << std::fixed << value; + std::string d = oss.str(); + size_t i = d.find_last_not_of('0'); + if(i != std::string::npos && i != d.size() - 1) { + if(d[i] == '.') + i++; + d = d.substr(0, i + 1); + } + return d.c_str(); + } + + struct Endianness + { + enum Arch + { + Big, + Little + }; + + static Arch which() { + int x = 1; + // casting any data pointer to char* is allowed + auto ptr = reinterpret_cast(&x); + if(*ptr) + return Little; + return Big; + } + }; +} // namespace + +namespace detail { + void my_memcpy(void* dest, const void* src, unsigned num) { memcpy(dest, src, num); } + + String rawMemoryToString(const void* object, unsigned size) { + // Reverse order for little endian architectures + int i = 0, end = static_cast(size), inc = 1; + if(Endianness::which() == Endianness::Little) { + i = end - 1; + end = inc = -1; + } + + unsigned const char* bytes = static_cast(object); + std::ostringstream oss; + oss << "0x" << std::setfill('0') << std::hex; + for(; i != end; i += inc) + oss << std::setw(2) << static_cast(bytes[i]); + return oss.str().c_str(); + } + + DOCTEST_THREAD_LOCAL std::ostringstream g_oss; // NOLINT(cert-err58-cpp) + + std::ostream* getTlsOss() { + g_oss.clear(); // there shouldn't be anything worth clearing in the flags + g_oss.str(""); // the slow way of resetting a string stream + //g_oss.seekp(0); // optimal reset - as seen here: https://stackoverflow.com/a/624291/3162383 + return &g_oss; + } + + String getTlsOssResult() { + //g_oss << std::ends; // needed - as shown here: https://stackoverflow.com/a/624291/3162383 + return g_oss.str().c_str(); + } + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace timer_large_integer +{ + +#if defined(DOCTEST_PLATFORM_WINDOWS) + typedef ULONGLONG type; +#else // DOCTEST_PLATFORM_WINDOWS + using namespace std; + typedef uint64_t type; +#endif // DOCTEST_PLATFORM_WINDOWS +} + +typedef timer_large_integer::type ticks_t; + +#ifdef DOCTEST_CONFIG_GETCURRENTTICKS + ticks_t getCurrentTicks() { return DOCTEST_CONFIG_GETCURRENTTICKS(); } +#elif defined(DOCTEST_PLATFORM_WINDOWS) + ticks_t getCurrentTicks() { + static LARGE_INTEGER hz = {0}, hzo = {0}; + if(!hz.QuadPart) { + QueryPerformanceFrequency(&hz); + QueryPerformanceCounter(&hzo); + } + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; + } +#else // DOCTEST_PLATFORM_WINDOWS + ticks_t getCurrentTicks() { + timeval t; + gettimeofday(&t, nullptr); + return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); + } +#endif // DOCTEST_PLATFORM_WINDOWS + + struct Timer + { + void start() { m_ticks = getCurrentTicks(); } + unsigned int getElapsedMicroseconds() const { + return static_cast(getCurrentTicks() - m_ticks); + } + //unsigned int getElapsedMilliseconds() const { + // return static_cast(getElapsedMicroseconds() / 1000); + //} + double getElapsedSeconds() const { return (getCurrentTicks() - m_ticks) / 1000000.0; } + + private: + ticks_t m_ticks = 0; + }; + + // this holds both parameters from the command line and runtime data for tests + struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats + { + std::atomic numAssertsCurrentTest_atomic; + std::atomic numAssertsFailedCurrentTest_atomic; + + std::vector> filters = decltype(filters)(9); // 9 different filters + + std::vector reporters_currently_used; + + const TestCase* currentTest = nullptr; + + assert_handler ah = nullptr; + + Timer timer; + + std::vector stringifiedContexts; // logging from INFO() due to an exception + + // stuff for subcases + std::vector subcasesStack; + std::set subcasesPassed; + int subcasesCurrentMaxLevel; + bool should_reenter; + std::atomic shouldLogCurrentException; + + void resetRunData() { + numTestCases = 0; + numTestCasesPassingFilters = 0; + numTestSuitesPassingFilters = 0; + numTestCasesFailed = 0; + numAsserts = 0; + numAssertsFailed = 0; + numAssertsCurrentTest = 0; + numAssertsFailedCurrentTest = 0; + } + + void finalizeTestCaseData() { + seconds = timer.getElapsedSeconds(); + + // update the non-atomic counters + numAsserts += numAssertsCurrentTest_atomic; + numAssertsFailed += numAssertsFailedCurrentTest_atomic; + numAssertsCurrentTest = numAssertsCurrentTest_atomic; + numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; + + if(numAssertsFailedCurrentTest) + failure_flags |= TestCaseFailureReason::AssertFailure; + + if(Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && + Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) + failure_flags |= TestCaseFailureReason::Timeout; + + if(currentTest->m_should_fail) { + if(failure_flags) { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; + } else { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; + } + } else if(failure_flags && currentTest->m_may_fail) { + failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; + } else if(currentTest->m_expected_failures > 0) { + if(numAssertsFailedCurrentTest == currentTest->m_expected_failures) { + failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; + } else { + failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; + } + } + + bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); + + // if any subcase has failed - the whole test case has failed + if(failure_flags && !ok_to_fail) + numTestCasesFailed++; + } + }; + + ContextState* g_cs = nullptr; + + // used to avoid locks for the debug output + // TODO: figure out if this is indeed necessary/correct - seems like either there still + // could be a race or that there wouldn't be a race even if using the context directly + DOCTEST_THREAD_LOCAL bool g_no_colors; + +#endif // DOCTEST_CONFIG_DISABLE +} // namespace detail + +void String::setOnHeap() { *reinterpret_cast(&buf[last]) = 128; } +void String::setLast(unsigned in) { buf[last] = char(in); } + +void String::copy(const String& other) { + using namespace std; + if(other.isOnStack()) { + memcpy(buf, other.buf, len); + } else { + setOnHeap(); + data.size = other.data.size; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + memcpy(data.ptr, other.data.ptr, data.size + 1); + } +} + +String::String() { + buf[0] = '\0'; + setLast(); +} + +String::~String() { + if(!isOnStack()) + delete[] data.ptr; +} + +String::String(const char* in) + : String(in, strlen(in)) {} + +String::String(const char* in, unsigned in_size) { + using namespace std; + if(in_size <= last) { + memcpy(buf, in, in_size + 1); + setLast(last - in_size); + } else { + setOnHeap(); + data.size = in_size; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + memcpy(data.ptr, in, in_size + 1); + } +} + +String::String(const String& other) { copy(other); } + +String& String::operator=(const String& other) { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + + copy(other); + } + + return *this; +} + +String& String::operator+=(const String& other) { + const unsigned my_old_size = size(); + const unsigned other_size = other.size(); + const unsigned total_size = my_old_size + other_size; + using namespace std; + if(isOnStack()) { + if(total_size < len) { + // append to the current stack space + memcpy(buf + my_old_size, other.c_str(), other_size + 1); + setLast(last - total_size); + } else { + // alloc new chunk + char* temp = new char[total_size + 1]; + // copy current data to new location before writing in the union + memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed + // update data in union + setOnHeap(); + data.size = total_size; + data.capacity = data.size + 1; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } else { + if(data.capacity > total_size) { + // append to the current heap block + data.size = total_size; + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } else { + // resize + data.capacity *= 2; + if(data.capacity <= total_size) + data.capacity = total_size + 1; + // alloc new chunk + char* temp = new char[data.capacity]; + // copy current data to new location before releasing it + memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed + // release old chunk + delete[] data.ptr; + // update the rest of the union members + data.size = total_size; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } + + return *this; +} + +String String::operator+(const String& other) const { return String(*this) += other; } + +String::String(String&& other) { + using namespace std; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); +} + +String& String::operator=(String&& other) { + using namespace std; + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); + } + return *this; +} + +char String::operator[](unsigned i) const { + return const_cast(this)->operator[](i); // NOLINT +} + +char& String::operator[](unsigned i) { + if(isOnStack()) + return reinterpret_cast(buf)[i]; + return data.ptr[i]; +} + +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") +unsigned String::size() const { + if(isOnStack()) + return last - (unsigned(buf[last]) & 31); // using "last" would work only if "len" is 32 + return data.size; +} +DOCTEST_GCC_SUPPRESS_WARNING_POP + +unsigned String::capacity() const { + if(isOnStack()) + return len; + return data.capacity; +} + +int String::compare(const char* other, bool no_case) const { + if(no_case) + return doctest::stricmp(c_str(), other); + return std::strcmp(c_str(), other); +} + +int String::compare(const String& other, bool no_case) const { + return compare(other.c_str(), no_case); +} + +// clang-format off +bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; } +bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; } +bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; } +bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; } +bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; } +bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; } +// clang-format on + +std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); } + +namespace { + void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) +} // namespace + +namespace Color { + std::ostream& operator<<(std::ostream& s, Color::Enum code) { + color_to_stream(s, code); + return s; + } +} // namespace Color + +// clang-format off +const char* assertString(assertType::Enum at) { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4062) // enum 'x' in switch of enum 'y' is not handled + switch(at) { //!OCLINT missing default in switch statements + case assertType::DT_WARN : return "WARN"; + case assertType::DT_CHECK : return "CHECK"; + case assertType::DT_REQUIRE : return "REQUIRE"; + + case assertType::DT_WARN_FALSE : return "WARN_FALSE"; + case assertType::DT_CHECK_FALSE : return "CHECK_FALSE"; + case assertType::DT_REQUIRE_FALSE : return "REQUIRE_FALSE"; + + case assertType::DT_WARN_THROWS : return "WARN_THROWS"; + case assertType::DT_CHECK_THROWS : return "CHECK_THROWS"; + case assertType::DT_REQUIRE_THROWS : return "REQUIRE_THROWS"; + + case assertType::DT_WARN_THROWS_AS : return "WARN_THROWS_AS"; + case assertType::DT_CHECK_THROWS_AS : return "CHECK_THROWS_AS"; + case assertType::DT_REQUIRE_THROWS_AS : return "REQUIRE_THROWS_AS"; + + case assertType::DT_WARN_THROWS_WITH : return "WARN_THROWS_WITH"; + case assertType::DT_CHECK_THROWS_WITH : return "CHECK_THROWS_WITH"; + case assertType::DT_REQUIRE_THROWS_WITH : return "REQUIRE_THROWS_WITH"; + + case assertType::DT_WARN_THROWS_WITH_AS : return "WARN_THROWS_WITH_AS"; + case assertType::DT_CHECK_THROWS_WITH_AS : return "CHECK_THROWS_WITH_AS"; + case assertType::DT_REQUIRE_THROWS_WITH_AS : return "REQUIRE_THROWS_WITH_AS"; + + case assertType::DT_WARN_NOTHROW : return "WARN_NOTHROW"; + case assertType::DT_CHECK_NOTHROW : return "CHECK_NOTHROW"; + case assertType::DT_REQUIRE_NOTHROW : return "REQUIRE_NOTHROW"; + + case assertType::DT_WARN_EQ : return "WARN_EQ"; + case assertType::DT_CHECK_EQ : return "CHECK_EQ"; + case assertType::DT_REQUIRE_EQ : return "REQUIRE_EQ"; + case assertType::DT_WARN_NE : return "WARN_NE"; + case assertType::DT_CHECK_NE : return "CHECK_NE"; + case assertType::DT_REQUIRE_NE : return "REQUIRE_NE"; + case assertType::DT_WARN_GT : return "WARN_GT"; + case assertType::DT_CHECK_GT : return "CHECK_GT"; + case assertType::DT_REQUIRE_GT : return "REQUIRE_GT"; + case assertType::DT_WARN_LT : return "WARN_LT"; + case assertType::DT_CHECK_LT : return "CHECK_LT"; + case assertType::DT_REQUIRE_LT : return "REQUIRE_LT"; + case assertType::DT_WARN_GE : return "WARN_GE"; + case assertType::DT_CHECK_GE : return "CHECK_GE"; + case assertType::DT_REQUIRE_GE : return "REQUIRE_GE"; + case assertType::DT_WARN_LE : return "WARN_LE"; + case assertType::DT_CHECK_LE : return "CHECK_LE"; + case assertType::DT_REQUIRE_LE : return "REQUIRE_LE"; + + case assertType::DT_WARN_UNARY : return "WARN_UNARY"; + case assertType::DT_CHECK_UNARY : return "CHECK_UNARY"; + case assertType::DT_REQUIRE_UNARY : return "REQUIRE_UNARY"; + case assertType::DT_WARN_UNARY_FALSE : return "WARN_UNARY_FALSE"; + case assertType::DT_CHECK_UNARY_FALSE : return "CHECK_UNARY_FALSE"; + case assertType::DT_REQUIRE_UNARY_FALSE : return "REQUIRE_UNARY_FALSE"; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + return ""; +} +// clang-format on + +const char* failureString(assertType::Enum at) { + if(at & assertType::is_warn) //!OCLINT bitwise operator in conditional + return "WARNING"; + if(at & assertType::is_check) //!OCLINT bitwise operator in conditional + return "ERROR"; + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return "FATAL ERROR"; + return ""; +} + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +// depending on the current options this will remove the path of filenames +const char* skipPathFromFilename(const char* file) { + if(getContextOptions()->no_path_in_filenames) { + auto back = std::strrchr(file, '\\'); + auto forward = std::strrchr(file, '/'); + if(back || forward) { + if(back > forward) + forward = back; + return forward + 1; + } + } + return file; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +bool SubcaseSignature::operator<(const SubcaseSignature& other) const { + if(m_line != other.m_line) + return m_line < other.m_line; + if(std::strcmp(m_file, other.m_file) != 0) + return std::strcmp(m_file, other.m_file) < 0; + return m_name.compare(other.m_name) < 0; +} + +IContextScope::IContextScope() = default; +IContextScope::~IContextScope() = default; + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(char* in) { return toString(static_cast(in)); } +String toString(const char* in) { return String("\"") + (in ? in : "{null string}") + "\""; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(bool in) { return in ? "true" : "false"; } +String toString(float in) { return fpToString(in, 5) + "f"; } +String toString(double in) { return fpToString(in, 10); } +String toString(double long in) { return fpToString(in, 15); } + +#define DOCTEST_TO_STRING_OVERLOAD(type, fmt) \ + String toString(type in) { \ + char buf[64]; \ + std::sprintf(buf, fmt, in); \ + return buf; \ + } + +DOCTEST_TO_STRING_OVERLOAD(char, "%d") +DOCTEST_TO_STRING_OVERLOAD(char signed, "%d") +DOCTEST_TO_STRING_OVERLOAD(char unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int short, "%d") +DOCTEST_TO_STRING_OVERLOAD(int short unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int, "%d") +DOCTEST_TO_STRING_OVERLOAD(unsigned, "%u") +DOCTEST_TO_STRING_OVERLOAD(int long, "%ld") +DOCTEST_TO_STRING_OVERLOAD(int long unsigned, "%lu") +DOCTEST_TO_STRING_OVERLOAD(int long long, "%lld") +DOCTEST_TO_STRING_OVERLOAD(int long long unsigned, "%llu") + +String toString(std::nullptr_t) { return "NULL"; } + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/onqtam/doctest/issues/183 +String toString(const std::string& in) { return in.c_str(); } +#endif // VS 2019 + +Approx::Approx(double value) + : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100) + , m_scale(1.0) + , m_value(value) {} + +Approx Approx::operator()(double value) const { + Approx approx(value); + approx.epsilon(m_epsilon); + approx.scale(m_scale); + return approx; +} + +Approx& Approx::epsilon(double newEpsilon) { + m_epsilon = newEpsilon; + return *this; +} +Approx& Approx::scale(double newScale) { + m_scale = newScale; + return *this; +} + +bool operator==(double lhs, const Approx& rhs) { + // Thanks to Richard Harris for his help refining this formula + return std::fabs(lhs - rhs.m_value) < + rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); +} +bool operator==(const Approx& lhs, double rhs) { return operator==(rhs, lhs); } +bool operator!=(double lhs, const Approx& rhs) { return !operator==(lhs, rhs); } +bool operator!=(const Approx& lhs, double rhs) { return !operator==(rhs, lhs); } +bool operator<=(double lhs, const Approx& rhs) { return lhs < rhs.m_value || lhs == rhs; } +bool operator<=(const Approx& lhs, double rhs) { return lhs.m_value < rhs || lhs == rhs; } +bool operator>=(double lhs, const Approx& rhs) { return lhs > rhs.m_value || lhs == rhs; } +bool operator>=(const Approx& lhs, double rhs) { return lhs.m_value > rhs || lhs == rhs; } +bool operator<(double lhs, const Approx& rhs) { return lhs < rhs.m_value && lhs != rhs; } +bool operator<(const Approx& lhs, double rhs) { return lhs.m_value < rhs && lhs != rhs; } +bool operator>(double lhs, const Approx& rhs) { return lhs > rhs.m_value && lhs != rhs; } +bool operator>(const Approx& lhs, double rhs) { return lhs.m_value > rhs && lhs != rhs; } + +String toString(const Approx& in) { + return String("Approx( ") + doctest::toString(in.m_value) + " )"; +} +const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); } + +} // namespace doctest + +#ifdef DOCTEST_CONFIG_DISABLE +namespace doctest { +Context::Context(int, const char* const*) {} +Context::~Context() = default; +void Context::applyCommandLine(int, const char* const*) {} +void Context::addFilter(const char*, const char*) {} +void Context::clearFilters() {} +void Context::setOption(const char*, int) {} +void Context::setOption(const char*, const char*) {} +bool Context::shouldExit() { return false; } +void Context::setAsDefaultForAssertsOutOfTestCases() {} +void Context::setAssertHandler(detail::assert_handler) {} +int Context::run() { return 0; } + +IReporter::~IReporter() = default; + +int IReporter::get_num_active_contexts() { return 0; } +const IContextScope* const* IReporter::get_active_contexts() { return nullptr; } +int IReporter::get_num_stringified_contexts() { return 0; } +const String* IReporter::get_stringified_contexts() { return nullptr; } + +int registerReporter(const char*, int, IReporter*) { return 0; } + +} // namespace doctest +#else // DOCTEST_CONFIG_DISABLE + +#if !defined(DOCTEST_CONFIG_COLORS_NONE) +#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_CONFIG_COLORS_WINDOWS +#else // linux +#define DOCTEST_CONFIG_COLORS_ANSI +#endif // platform +#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI +#endif // DOCTEST_CONFIG_COLORS_NONE + +namespace doctest_detail_test_suite_ns { +// holds the current test suite +doctest::detail::TestSuite& getCurrentTestSuite() { + static doctest::detail::TestSuite data; + return data; +} +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +namespace { + // the int (priority) is part of the key for automatic sorting - sadly one can register a + // reporter with a duplicate name and a different priority but hopefully that won't happen often :| + typedef std::map, reporterCreatorFunc> reporterMap; + + reporterMap& getReporters() { + static reporterMap data; + return data; + } + reporterMap& getListeners() { + static reporterMap data; + return data; + } +} // namespace +namespace detail { +#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ + for(auto& curr_rep : g_cs->reporters_currently_used) \ + curr_rep->function(__VA_ARGS__) + + bool checkIfShouldThrow(assertType::Enum at) { + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return true; + + if((at & assertType::is_check) //!OCLINT bitwise operator in conditional + && getContextOptions()->abort_after > 0 && + (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= + getContextOptions()->abort_after) + return true; + + return false; + } + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + [[noreturn]] void throwException() { + g_cs->shouldLogCurrentException = false; + throw TestFailureException(); + } // NOLINT(cert-err60-cpp) +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + void throwException() {} +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} // namespace detail + +namespace { + using namespace detail; + // matching of a string against a wildcard mask (case sensitivity configurable) taken from + // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing + int wildcmp(const char* str, const char* wild, bool caseSensitive) { + const char* cp = nullptr; + const char* mp = nullptr; + + while((*str) && (*wild != '*')) { + if((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && + (*wild != '?')) { + return 0; + } + wild++; + str++; + } + + while(*str) { + if(*wild == '*') { + if(!*++wild) { + return 1; + } + mp = wild; + cp = str + 1; + } else if((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || + (*wild == '?')) { + wild++; + str++; + } else { + wild = mp; //!OCLINT parameter reassignment + str = cp++; //!OCLINT parameter reassignment + } + } + + while(*wild == '*') { + wild++; + } + return !*wild; + } + + //// C string hash function (djb2) - taken from http://www.cse.yorku.ca/~oz/hash.html + //unsigned hashStr(unsigned const char* str) { + // unsigned long hash = 5381; + // char c; + // while((c = *str++)) + // hash = ((hash << 5) + hash) + c; // hash * 33 + c + // return hash; + //} + + // checks if the name matches any of the filters (and can be configured what to do when empty) + bool matchesAny(const char* name, const std::vector& filters, bool matchEmpty, + bool caseSensitive) { + if(filters.empty() && matchEmpty) + return true; + for(auto& curr : filters) + if(wildcmp(name, curr.c_str(), caseSensitive)) + return true; + return false; + } +} // namespace +namespace detail { + + Subcase::Subcase(const String& name, const char* file, int line) + : m_signature({name, file, line}) { + ContextState* s = g_cs; + + // check subcase filters + if(s->subcasesStack.size() < size_t(s->subcase_filter_levels)) { + if(!matchesAny(m_signature.m_name.c_str(), s->filters[6], true, s->case_sensitive)) + return; + if(matchesAny(m_signature.m_name.c_str(), s->filters[7], false, s->case_sensitive)) + return; + } + + // if a Subcase on the same level has already been entered + if(s->subcasesStack.size() < size_t(s->subcasesCurrentMaxLevel)) { + s->should_reenter = true; + return; + } + + // push the current signature to the stack so we can check if the + // current stack + the current new subcase have been traversed + s->subcasesStack.push_back(m_signature); + if(s->subcasesPassed.count(s->subcasesStack) != 0) { + // pop - revert to previous stack since we've already passed this + s->subcasesStack.pop_back(); + return; + } + + s->subcasesCurrentMaxLevel = s->subcasesStack.size(); + m_entered = true; + + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); + } + + Subcase::~Subcase() { + if(m_entered) { + // only mark the subcase stack as passed if no subcases have been skipped + if(g_cs->should_reenter == false) + g_cs->subcasesPassed.insert(g_cs->subcasesStack); + g_cs->subcasesStack.pop_back(); + +#if __cplusplus >= 201703L && defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411 + if(std::uncaught_exceptions() > 0 +#else + if(std::uncaught_exception() +#endif + && g_cs->shouldLogCurrentException) { + DOCTEST_ITERATE_THROUGH_REPORTERS( + test_case_exception, {"exception thrown in subcase - will translate later " + "when the whole test case has been exited (cannot " + "translate while there is an active exception)", + false}); + g_cs->shouldLogCurrentException = false; + } + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + } + + Subcase::operator bool() const { return m_entered; } + + Result::Result(bool passed, const String& decomposition) + : m_passed(passed) + , m_decomp(decomposition) {} + + ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) + : m_at(at) {} + + TestSuite& TestSuite::operator*(const char* in) { + m_test_suite = in; + // clear state + m_description = nullptr; + m_skip = false; + m_may_fail = false; + m_should_fail = false; + m_expected_failures = 0; + m_timeout = 0; + return *this; + } + + TestCase::TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const char* type, int template_id) { + m_file = file; + m_line = line; + m_name = nullptr; // will be later overridden in operator* + m_test_suite = test_suite.m_test_suite; + m_description = test_suite.m_description; + m_skip = test_suite.m_skip; + m_may_fail = test_suite.m_may_fail; + m_should_fail = test_suite.m_should_fail; + m_expected_failures = test_suite.m_expected_failures; + m_timeout = test_suite.m_timeout; + + m_test = test; + m_type = type; + m_template_id = template_id; + } + + TestCase::TestCase(const TestCase& other) + : TestCaseData() { + *this = other; + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + DOCTEST_MSVC_SUPPRESS_WARNING(26437) // Do not slice + TestCase& TestCase::operator=(const TestCase& other) { + static_cast(*this) = static_cast(other); + + m_test = other.m_test; + m_type = other.m_type; + m_template_id = other.m_template_id; + m_full_name = other.m_full_name; + + if(m_template_id != -1) + m_name = m_full_name.c_str(); + return *this; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& TestCase::operator*(const char* in) { + m_name = in; + // make a new name with an appended type for templated test case + if(m_template_id != -1) { + m_full_name = String(m_name) + m_type; + // redirect the name to point to the newly constructed full name + m_name = m_full_name.c_str(); + } + return *this; + } + + bool TestCase::operator<(const TestCase& other) const { + if(m_line != other.m_line) + return m_line < other.m_line; + const int file_cmp = std::strcmp(m_file, other.m_file); + if(file_cmp != 0) + return file_cmp < 0; + return m_template_id < other.m_template_id; + } +} // namespace detail +namespace { + using namespace detail; + // for sorting tests by file/line + bool fileOrderComparator(const TestCase* lhs, const TestCase* rhs) { +#if DOCTEST_MSVC + // this is needed because MSVC gives different case for drive letters + // for __FILE__ when evaluated in a header and a source file + const int res = doctest::stricmp(lhs->m_file, rhs->m_file); +#else // MSVC + const int res = std::strcmp(lhs->m_file, rhs->m_file); +#endif // MSVC + if(res != 0) + return res < 0; + if(lhs->m_line != rhs->m_line) + return lhs->m_line < rhs->m_line; + return lhs->m_template_id < rhs->m_template_id; + } + + // for sorting tests by suite/file/line + bool suiteOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); + if(res != 0) + return res < 0; + return fileOrderComparator(lhs, rhs); + } + + // for sorting tests by name/suite/file/line + bool nameOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_name, rhs->m_name); + if(res != 0) + return res < 0; + return suiteOrderComparator(lhs, rhs); + } + + // all the registered tests + std::set& getRegisteredTests() { + static std::set data; + return data; + } + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + HANDLE g_stdoutHandle; + WORD g_origFgAttrs; + WORD g_origBgAttrs; + bool g_attrsInitted = false; + + int colors_init() { + if(!g_attrsInitted) { + g_stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + g_attrsInitted = true; + CONSOLE_SCREEN_BUFFER_INFO csbiInfo; + GetConsoleScreenBufferInfo(g_stdoutHandle, &csbiInfo); + g_origFgAttrs = csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | + BACKGROUND_BLUE | BACKGROUND_INTENSITY); + g_origBgAttrs = csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | + FOREGROUND_BLUE | FOREGROUND_INTENSITY); + } + return 0; + } + + int dumy_init_console_colors = colors_init(); +#endif // DOCTEST_CONFIG_COLORS_WINDOWS + + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + void color_to_stream(std::ostream& s, Color::Enum code) { + ((void)s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS + ((void)code); // for DOCTEST_CONFIG_COLORS_NONE +#ifdef DOCTEST_CONFIG_COLORS_ANSI + if(g_no_colors || + (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) + return; + + auto col = ""; + // clang-format off + switch(code) { //!OCLINT missing break in switch statement / unnecessary default statement in covered switch statement + case Color::Red: col = "[0;31m"; break; + case Color::Green: col = "[0;32m"; break; + case Color::Blue: col = "[0;34m"; break; + case Color::Cyan: col = "[0;36m"; break; + case Color::Yellow: col = "[0;33m"; break; + case Color::Grey: col = "[1;30m"; break; + case Color::LightGrey: col = "[0;37m"; break; + case Color::BrightRed: col = "[1;31m"; break; + case Color::BrightGreen: col = "[1;32m"; break; + case Color::BrightWhite: col = "[1;37m"; break; + case Color::Bright: // invalid + case Color::None: + case Color::White: + default: col = "[0m"; + } + // clang-format on + s << "\033" << col; +#endif // DOCTEST_CONFIG_COLORS_ANSI + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + if(g_no_colors || + (isatty(fileno(stdout)) == false && getContextOptions()->force_colors == false)) + return; + +#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(g_stdoutHandle, x | g_origBgAttrs) + + // clang-format off + switch (code) { + case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; + case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; + case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; + case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; + case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; + case Color::Grey: DOCTEST_SET_ATTR(0); break; + case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; + case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; + case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; + case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::None: + case Color::Bright: // invalid + default: DOCTEST_SET_ATTR(g_origFgAttrs); + } + // clang-format on +#endif // DOCTEST_CONFIG_COLORS_WINDOWS + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + + std::vector& getExceptionTranslators() { + static std::vector data; + return data; + } + + String translateActiveException() { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + String res; + auto& translators = getExceptionTranslators(); + for(auto& curr : translators) + if(curr->translate(res)) + return res; + // clang-format off + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") + try { + throw; + } catch(std::exception& ex) { + return ex.what(); + } catch(std::string& msg) { + return msg.c_str(); + } catch(const char* msg) { + return msg; + } catch(...) { + return "unknown exception"; + } + DOCTEST_GCC_SUPPRESS_WARNING_POP +// clang-format on +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + return ""; +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } +} // namespace + +namespace detail { + // used by the macros for registering tests + int regTest(const TestCase& tc) { + getRegisteredTests().insert(tc); + return 0; + } + + // sets the current test suite + int setTestSuite(const TestSuite& ts) { + doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; + return 0; + } + +#ifdef DOCTEST_IS_DEBUGGER_ACTIVE + bool isDebuggerActive() { return DOCTEST_IS_DEBUGGER_ACTIVE(); } +#else // DOCTEST_IS_DEBUGGER_ACTIVE +#ifdef DOCTEST_PLATFORM_MAC + // The following function is taken directly from the following technical note: + // https://developer.apple.com/library/archive/qa/qa1361/_index.html + // Returns true if the current process is being debugged (either + // running under the debugger or has a debugger attached post facto). + bool isDebuggerActive() { + int mib[4]; + kinfo_proc info; + size_t size; + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + // Call sysctl. + size = sizeof(info); + if(sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, 0, 0) != 0) { + std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; + return false; + } + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); + } +#elif DOCTEST_MSVC || defined(__MINGW32__) + bool isDebuggerActive() { return ::IsDebuggerPresent() != 0; } +#else + bool isDebuggerActive() { return false; } +#endif // Platform +#endif // DOCTEST_IS_DEBUGGER_ACTIVE + + void registerExceptionTranslatorImpl(const IExceptionTranslator* et) { + if(std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == + getExceptionTranslators().end()) + getExceptionTranslators().push_back(et); + } + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + void toStream(std::ostream* s, char* in) { *s << in; } + void toStream(std::ostream* s, const char* in) { *s << in; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + void toStream(std::ostream* s, bool in) { *s << std::boolalpha << in << std::noboolalpha; } + void toStream(std::ostream* s, float in) { *s << in; } + void toStream(std::ostream* s, double in) { *s << in; } + void toStream(std::ostream* s, double long in) { *s << in; } + + void toStream(std::ostream* s, char in) { *s << in; } + void toStream(std::ostream* s, char signed in) { *s << in; } + void toStream(std::ostream* s, char unsigned in) { *s << in; } + void toStream(std::ostream* s, int short in) { *s << in; } + void toStream(std::ostream* s, int short unsigned in) { *s << in; } + void toStream(std::ostream* s, int in) { *s << in; } + void toStream(std::ostream* s, int unsigned in) { *s << in; } + void toStream(std::ostream* s, int long in) { *s << in; } + void toStream(std::ostream* s, int long unsigned in) { *s << in; } + void toStream(std::ostream* s, int long long in) { *s << in; } + void toStream(std::ostream* s, int long long unsigned in) { *s << in; } + + DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() + + ContextScopeBase::ContextScopeBase() { + g_infoContexts.push_back(this); + } + + // destroy cannot be inlined into the destructor because that would mean calling stringify after + // ContextScope has been destroyed (base class destructors run after derived class destructors). + // Instead, ContextScope calls this method directly from its destructor. + void ContextScopeBase::destroy() { +#if __cplusplus >= 201703L && defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411 + if(std::uncaught_exceptions() > 0) { +#else + if(std::uncaught_exception()) { +#endif + std::ostringstream s; + this->stringify(&s); + g_cs->stringifiedContexts.push_back(s.str().c_str()); + } + g_infoContexts.pop_back(); + } + +} // namespace detail +namespace { + using namespace detail; + + std::ostream& file_line_to_stream(std::ostream& s, const char* file, int line, + const char* tail = "") { + const auto opt = getContextOptions(); + s << Color::LightGrey << skipPathFromFilename(file) << (opt->gnu_file_line ? ":" : "(") + << (opt->no_line_numbers ? 0 : line) // 0 or the real num depending on the option + << (opt->gnu_file_line ? ":" : "):") << tail; + return s; + } + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) + struct FatalConditionHandler + { + void reset() {} + }; +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + + void reportFatal(const std::string&); + +#ifdef DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + DWORD id; + const char* name; + }; + // There is no 1-1 mapping between signals and windows exceptions. + // Windows can easily distinguish between SO and SigSegV, + // but SigInt, SigTerm, etc are handled differently. + SignalDefs signalDefs[] = { + {EXCEPTION_ILLEGAL_INSTRUCTION, "SIGILL - Illegal instruction signal"}, + {EXCEPTION_STACK_OVERFLOW, "SIGSEGV - Stack overflow"}, + {EXCEPTION_ACCESS_VIOLATION, "SIGSEGV - Segmentation violation signal"}, + {EXCEPTION_INT_DIVIDE_BY_ZERO, "Divide by zero error"}, + }; + + struct FatalConditionHandler + { + static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo) { + for(size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + if(ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { + reportFatal(signalDefs[i].name); + break; + } + } + // If its not an exception we care about, pass it along. + // This stops us from eating debugger breaks etc. + return EXCEPTION_CONTINUE_SEARCH; + } + + FatalConditionHandler() { + isSet = true; + // 32k seems enough for doctest to handle stack overflow, + // but the value was found experimentally, so there is no strong guarantee + guaranteeSize = 32 * 1024; + // Register an unhandled exception filter + previousTop = SetUnhandledExceptionFilter(handleException); + // Pass in guarantee size to be filled + SetThreadStackGuarantee(&guaranteeSize); + } + + static void reset() { + if(isSet) { + // Unregister handler and restore the old guarantee + SetUnhandledExceptionFilter(previousTop); + SetThreadStackGuarantee(&guaranteeSize); + previousTop = nullptr; + isSet = false; + } + } + + ~FatalConditionHandler() { reset(); } + + private: + static bool isSet; + static ULONG guaranteeSize; + static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; + }; + + bool FatalConditionHandler::isSet = false; + ULONG FatalConditionHandler::guaranteeSize = 0; + LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; + +#else // DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + int id; + const char* name; + }; + SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"}, + {SIGILL, "SIGILL - Illegal instruction signal"}, + {SIGFPE, "SIGFPE - Floating point error signal"}, + {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, + {SIGTERM, "SIGTERM - Termination request signal"}, + {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}}; + + struct FatalConditionHandler + { + static bool isSet; + static struct sigaction oldSigActions[DOCTEST_COUNTOF(signalDefs)]; + static stack_t oldSigStack; + static char altStackMem[4 * SIGSTKSZ]; + + static void handleSignal(int sig) { + const char* name = ""; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + SignalDefs& def = signalDefs[i]; + if(sig == def.id) { + name = def.name; + break; + } + } + reset(); + reportFatal(name); + raise(sig); + } + + FatalConditionHandler() { + isSet = true; + stack_t sigStack; + sigStack.ss_sp = altStackMem; + sigStack.ss_size = sizeof(altStackMem); + sigStack.ss_flags = 0; + sigaltstack(&sigStack, &oldSigStack); + struct sigaction sa = {}; + sa.sa_handler = handleSignal; // NOLINT + sa.sa_flags = SA_ONSTACK; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); + } + } + + ~FatalConditionHandler() { reset(); } + static void reset() { + if(isSet) { + // Set signals back to previous values -- hopefully nobody overwrote them in the meantime + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); + } + // Return the old stack + sigaltstack(&oldSigStack, nullptr); + isSet = false; + } + } + }; + + bool FatalConditionHandler::isSet = false; + struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; + stack_t FatalConditionHandler::oldSigStack = {}; + char FatalConditionHandler::altStackMem[] = {}; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace + +namespace { + using namespace detail; + +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) +#else + // TODO: integration with XCode and other IDEs +#define DOCTEST_OUTPUT_DEBUG_STRING(text) // NOLINT(clang-diagnostic-unused-macros) +#endif // Platform + + void addAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsCurrentTest_atomic++; + } + + void addFailedAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsFailedCurrentTest_atomic++; + } + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) + void reportFatal(const std::string& message) { + g_cs->failure_flags |= TestCaseFailureReason::Crash; + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); + + while(g_cs->subcasesStack.size()) { + g_cs->subcasesStack.pop_back(); + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + + g_cs->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH +} // namespace +namespace detail { + + ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const char* exception_string) { + m_test_case = g_cs->currentTest; + m_at = at; + m_file = file; + m_line = line; + m_expr = expr; + m_failed = true; + m_threw = false; + m_threw_as = false; + m_exception_type = exception_type; + m_exception_string = exception_string; +#if DOCTEST_MSVC + if(m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC + ++m_expr; +#endif // MSVC + } + + void ResultBuilder::setResult(const Result& res) { + m_decomp = res.m_decomp; + m_failed = !res.m_passed; + } + + void ResultBuilder::translateException() { + m_threw = true; + m_exception = translateActiveException(); + } + + bool ResultBuilder::log() { + if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw; + } else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT + m_failed = !m_threw_as || (m_exception != m_exception_string); + } else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw_as; + } else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + m_failed = m_exception != m_exception_string; + } else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + m_failed = m_threw; + } + + if(m_exception.size()) + m_exception = String("\"") + m_exception + "\""; + + if(is_running_in_test) { + addAssert(m_at); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); + + if(m_failed) + addFailedAssert(m_at); + } else if(m_failed) { + failed_out_of_a_testing_context(*this); + } + + return m_failed && isDebuggerActive() && + !getContextOptions()->no_breaks; // break into debugger + } + + void ResultBuilder::react() const { + if(m_failed && checkIfShouldThrow(m_at)) + throwException(); + } + + void failed_out_of_a_testing_context(const AssertData& ad) { + if(g_cs->ah) + g_cs->ah(ad); + else + std::abort(); + } + + void decomp_assert(assertType::Enum at, const char* file, int line, const char* expr, + Result result) { + bool failed = !result.m_passed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); + DOCTEST_ASSERT_IN_TESTS(result.m_decomp); + } + + MessageBuilder::MessageBuilder(const char* file, int line, assertType::Enum severity) { + m_stream = getTlsOss(); + m_file = file; + m_line = line; + m_severity = severity; + } + + IExceptionTranslator::IExceptionTranslator() = default; + IExceptionTranslator::~IExceptionTranslator() = default; + + bool MessageBuilder::log() { + m_string = getTlsOssResult(); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); + + const bool isWarn = m_severity & assertType::is_warn; + + // warn is just a message in this context so we don't treat it as an assert + if(!isWarn) { + addAssert(m_severity); + addFailedAssert(m_severity); + } + + return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn; // break + } + + void MessageBuilder::react() { + if(m_severity & assertType::is_require) //!OCLINT bitwise operator in conditional + throwException(); + } + + MessageBuilder::~MessageBuilder() = default; +} // namespace detail +namespace { + using namespace detail; + + template + [[noreturn]] void throw_exception(Ex const& e) { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + throw e; +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + std::cerr << "doctest will terminate because it needed to throw an exception.\n" + << "The message was: " << e.what() << '\n'; + std::terminate(); +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } + +#define DOCTEST_INTERNAL_ERROR(msg) \ + throw_exception(std::logic_error( \ + __FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) + + // clang-format off + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + + class XmlEncode { + public: + enum ForWhat { ForTextNodes, ForAttributes }; + + XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); + + void encodeTo( std::ostream& os ) const; + + friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); + + private: + std::string m_str; + ForWhat m_forWhat; + }; + + class XmlWriter { + public: + + class ScopedElement { + public: + ScopedElement( XmlWriter* writer ); + + ScopedElement( ScopedElement&& other ) noexcept; + ScopedElement& operator=( ScopedElement&& other ) noexcept; + + ~ScopedElement(); + + ScopedElement& writeText( std::string const& text, bool indent = true ); + + template + ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { + m_writer->writeAttribute( name, attribute ); + return *this; + } + + private: + mutable XmlWriter* m_writer = nullptr; + }; + + XmlWriter( std::ostream& os = std::cout ); + ~XmlWriter(); + + XmlWriter( XmlWriter const& ) = delete; + XmlWriter& operator=( XmlWriter const& ) = delete; + + XmlWriter& startElement( std::string const& name ); + + ScopedElement scopedElement( std::string const& name ); + + XmlWriter& endElement(); + + XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); + + XmlWriter& writeAttribute( std::string const& name, const char* attribute ); + + XmlWriter& writeAttribute( std::string const& name, bool attribute ); + + template + XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { + std::stringstream rss; + rss << attribute; + return writeAttribute( name, rss.str() ); + } + + XmlWriter& writeText( std::string const& text, bool indent = true ); + + //XmlWriter& writeComment( std::string const& text ); + + //void writeStylesheetRef( std::string const& url ); + + //XmlWriter& writeBlankLine(); + + void ensureTagClosed(); + + private: + + void writeDeclaration(); + + void newlineIfNecessary(); + + bool m_tagIsOpen = false; + bool m_needsNewline = false; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + +using uchar = unsigned char; + +namespace { + + size_t trailingBytes(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return 2; + } + if ((c & 0xF0) == 0xE0) { + return 3; + } + if ((c & 0xF8) == 0xF0) { + return 4; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + uint32_t headerValue(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return c & 0x1F; + } + if ((c & 0xF0) == 0xE0) { + return c & 0x0F; + } + if ((c & 0xF8) == 0xF0) { + return c & 0x07; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + void hexEscapeChar(std::ostream& os, unsigned char c) { + std::ios_base::fmtflags f(os.flags()); + os << "\\x" + << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(c); + os.flags(f); + } + +} // anonymous namespace + + XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) + : m_str( str ), + m_forWhat( forWhat ) + {} + + void XmlEncode::encodeTo( std::ostream& os ) const { + // Apostrophe escaping not necessary if we always use " to write attributes + // (see: https://www.w3.org/TR/xml/#syntax) + + for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { + uchar c = m_str[idx]; + switch (c) { + case '<': os << "<"; break; + case '&': os << "&"; break; + + case '>': + // See: https://www.w3.org/TR/xml/#syntax + if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') + os << ">"; + else + os << c; + break; + + case '\"': + if (m_forWhat == ForAttributes) + os << """; + else + os << c; + break; + + default: + // Check for control characters and invalid utf-8 + + // Escape control characters in standard ascii + // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { + hexEscapeChar(os, c); + break; + } + + // Plain ASCII: Write it to stream + if (c < 0x7F) { + os << c; + break; + } + + // UTF-8 territory + // Check if the encoding is valid and if it is not, hex escape bytes. + // Important: We do not check the exact decoded values for validity, only the encoding format + // First check that this bytes is a valid lead byte: + // This means that it is not encoded as 1111 1XXX + // Or as 10XX XXXX + if (c < 0xC0 || + c >= 0xF8) { + hexEscapeChar(os, c); + break; + } + + auto encBytes = trailingBytes(c); + // Are there enough bytes left to avoid accessing out-of-bounds memory? + if (idx + encBytes - 1 >= m_str.size()) { + hexEscapeChar(os, c); + break; + } + // The header is valid, check data + // The next encBytes bytes must together be a valid utf-8 + // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + bool valid = true; + uint32_t value = headerValue(c); + for (std::size_t n = 1; n < encBytes; ++n) { + uchar nc = m_str[idx + n]; + valid &= ((nc & 0xC0) == 0x80); + value = (value << 6) | (nc & 0x3F); + } + + if ( + // Wrong bit pattern of following bytes + (!valid) || + // Overlong encodings + (value < 0x80) || + ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant + (0x800 < value && value < 0x10000 && encBytes > 3) || + // Encoded value out of range + (value >= 0x110000) + ) { + hexEscapeChar(os, c); + break; + } + + // If we got here, this is in fact a valid(ish) utf-8 sequence + for (std::size_t n = 0; n < encBytes; ++n) { + os << m_str[idx + n]; + } + idx += encBytes - 1; + break; + } + } + } + + std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { + xmlEncode.encodeTo( os ); + return os; + } + + XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + {} + + XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) noexcept + : m_writer( other.m_writer ){ + other.m_writer = nullptr; + } + XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) noexcept { + if ( m_writer ) { + m_writer->endElement(); + } + m_writer = other.m_writer; + other.m_writer = nullptr; + return *this; + } + + + XmlWriter::ScopedElement::~ScopedElement() { + if( m_writer ) + m_writer->endElement(); + } + + XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { + m_writer->writeText( text, indent ); + return *this; + } + + XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) + { + writeDeclaration(); + } + + XmlWriter::~XmlWriter() { + while( !m_tags.empty() ) + endElement(); + } + + XmlWriter& XmlWriter::startElement( std::string const& name ) { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << '<' << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& XmlWriter::endElement() { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) { + m_os << "/>"; + m_tagIsOpen = false; + } + else { + m_os << m_indent << ""; + } + m_os << std::endl; + m_tags.pop_back(); + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { + if( !name.empty() && !attribute.empty() ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { + if( !name.empty() && attribute && attribute[0] != '\0' ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { + m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { + if( !text.empty() ){ + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen && indent ) + m_os << m_indent; + m_os << XmlEncode( text ); + m_needsNewline = true; + } + return *this; + } + + //XmlWriter& XmlWriter::writeComment( std::string const& text ) { + // ensureTagClosed(); + // m_os << m_indent << ""; + // m_needsNewline = true; + // 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::endl; + m_tagIsOpen = false; + } + } + + void XmlWriter::writeDeclaration() { + m_os << "\n"; + } + + void XmlWriter::newlineIfNecessary() { + if( m_needsNewline ) { + m_os << std::endl; + m_needsNewline = false; + } + } + +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + + // clang-format on + + struct XmlReporter : public IReporter + { + XmlWriter xml; + std::mutex mutex; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + XmlReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + std::stringstream ss; + for(int i = 0; i < num_contexts; ++i) { + contexts[i]->stringify(&ss); + xml.scopedElement("Info").writeText(ss.str()); + ss.str(""); + } + } + } + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + void test_case_start_impl(const TestCaseData& in) { + bool open_ts_tag = false; + if(tc != nullptr) { // we have already opened a test suite + if(std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { + xml.endElement(); + open_ts_tag = true; + } + } + else { + open_ts_tag = true; // first test case ==> first test suite + } + + if(open_ts_tag) { + xml.startElement("TestSuite"); + xml.writeAttribute("name", in.m_test_suite); + } + + tc = ∈ + xml.startElement("TestCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)) + .writeAttribute("description", in.m_description); + + if(Approx(in.m_timeout) != 0) + xml.writeAttribute("timeout", in.m_timeout); + if(in.m_may_fail) + xml.writeAttribute("may_fail", true); + if(in.m_should_fail) + xml.writeAttribute("should_fail", true); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + test_run_start(); + if(opt.list_reporters) { + for(auto& curr : getListeners()) + xml.scopedElement("Listener") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + for(auto& curr : getReporters()) + xml.scopedElement("Reporter") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + } else if(opt.count || opt.list_test_cases) { + for(unsigned i = 0; i < in.num_data; ++i) { + xml.scopedElement("TestCase").writeAttribute("name", in.data[i]->m_name) + .writeAttribute("testsuite", in.data[i]->m_test_suite) + .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file)) + .writeAttribute("line", line(in.data[i]->m_line)); + } + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + } else if(opt.list_test_suites) { + for(unsigned i = 0; i < in.num_data; ++i) + xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + xml.scopedElement("OverallResultsTestSuites") + .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); + } + xml.endElement(); + } + + void test_run_start() override { + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("doctest").writeAttribute("binary", binary_name); + if(opt.no_version == false) + xml.writeAttribute("version", DOCTEST_VERSION_STR); + + // only the consequential ones (TODO: filters) + xml.scopedElement("Options") + .writeAttribute("order_by", opt.order_by.c_str()) + .writeAttribute("rand_seed", opt.rand_seed) + .writeAttribute("first", opt.first) + .writeAttribute("last", opt.last) + .writeAttribute("abort_after", opt.abort_after) + .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) + .writeAttribute("case_sensitive", opt.case_sensitive) + .writeAttribute("no_throw", opt.no_throw) + .writeAttribute("no_skip", opt.no_skip); + } + + void test_run_end(const TestRunStats& p) override { + if(tc) // the TestSuite tag - only if there has been at least 1 test case + xml.endElement(); + + xml.scopedElement("OverallResultsAsserts") + .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) + .writeAttribute("failures", p.numAssertsFailed); + + xml.startElement("OverallResultsTestCases") + .writeAttribute("successes", + p.numTestCasesPassingFilters - p.numTestCasesFailed) + .writeAttribute("failures", p.numTestCasesFailed); + if(opt.no_skipped_summary == false) + xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); + xml.endElement(); + + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + test_case_start_impl(in); + xml.ensureTagClosed(); + } + + void test_case_reenter(const TestCaseData&) override {} + + void test_case_end(const CurrentTestCaseStats& st) override { + xml.startElement("OverallResultsAsserts") + .writeAttribute("successes", + st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) + .writeAttribute("failures", st.numAssertsFailedCurrentTest); + if(opt.duration) + xml.writeAttribute("duration", st.seconds); + if(tc->m_expected_failures) + xml.writeAttribute("expected_failures", tc->m_expected_failures); + xml.endElement(); + + xml.endElement(); + } + + void test_case_exception(const TestCaseException& e) override { + std::lock_guard lock(mutex); + + xml.scopedElement("Exception") + .writeAttribute("crash", e.is_crash) + .writeText(e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + std::lock_guard lock(mutex); + + xml.startElement("SubCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)); + xml.ensureTagClosed(); + } + + void subcase_end() override { xml.endElement(); } + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed && !opt.success) + return; + + std::lock_guard lock(mutex); + + xml.startElement("Expression") + .writeAttribute("success", !rb.m_failed) + .writeAttribute("type", assertString(rb.m_at)) + .writeAttribute("filename", skipPathFromFilename(rb.m_file)) + .writeAttribute("line", line(rb.m_line)); + + xml.scopedElement("Original").writeText(rb.m_expr); + + if(rb.m_threw) + xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); + + if(rb.m_at & assertType::is_throws_as) + xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); + if(rb.m_at & assertType::is_throws_with) + xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string); + if((rb.m_at & assertType::is_normal) && !rb.m_threw) + xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void log_message(const MessageData& mb) override { + std::lock_guard lock(mutex); + + xml.startElement("Message") + .writeAttribute("type", failureString(mb.m_severity)) + .writeAttribute("filename", skipPathFromFilename(mb.m_file)) + .writeAttribute("line", line(mb.m_line)); + + xml.scopedElement("Text").writeText(mb.m_string.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void test_case_skipped(const TestCaseData& in) override { + if(opt.no_skipped_summary == false) { + test_case_start_impl(in); + xml.writeAttribute("skipped", "true"); + xml.endElement(); + } + } + }; + + DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); + + struct Whitespace + { + int nrSpaces; + explicit Whitespace(int nr) + : nrSpaces(nr) {} + }; + + std::ostream& operator<<(std::ostream& out, const Whitespace& ws) { + if(ws.nrSpaces != 0) + out << std::setw(ws.nrSpaces) << ' '; + return out; + } + + struct ConsoleReporter : public IReporter + { + std::ostream& s; + bool hasLoggedCurrentTestStart; + std::vector subcasesStack; + std::mutex mutex; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc; + + ConsoleReporter(const ContextOptions& co) + : s(*co.cout) + , opt(co) {} + + ConsoleReporter(const ContextOptions& co, std::ostream& ostr) + : s(ostr) + , opt(co) {} + + // ========================================================================================= + // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE + // ========================================================================================= + + void separator_to_stream() { + s << Color::Yellow + << "===============================================================================" + "\n"; + } + + const char* getSuccessOrFailString(bool success, assertType::Enum at, + const char* success_str) { + if(success) + return success_str; + return failureString(at); + } + + Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at) { + return success ? Color::BrightGreen : + (at & assertType::is_warn) ? Color::Yellow : Color::Red; + } + + void successOrFailColoredStringToStream(bool success, assertType::Enum at, + const char* success_str = "SUCCESS") { + s << getSuccessOrFailColor(success, at) + << getSuccessOrFailString(success, at, success_str) << ": "; + } + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << Color::None << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << "\n"; + } + } + + s << "\n"; + } + + void logTestStart() { + if(hasLoggedCurrentTestStart) + return; + + separator_to_stream(); + file_line_to_stream(s, tc->m_file, tc->m_line, "\n"); + if(tc->m_description) + s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; + if(tc->m_test_suite && tc->m_test_suite[0] != '\0') + s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; + if(strncmp(tc->m_name, " Scenario:", 11) != 0) + s << Color::None << "TEST CASE: "; + s << Color::None << tc->m_name << "\n"; + + for(auto& curr : subcasesStack) + if(curr.m_name[0] != '\0') + s << " " << curr.m_name << "\n"; + + s << "\n"; + + hasLoggedCurrentTestStart = true; + } + + void printVersion() { + if(opt.no_version == false) + s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" + << DOCTEST_VERSION_STR << "\"\n"; + } + + void printIntro() { + printVersion(); + s << Color::Cyan << "[doctest] " << Color::None + << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; + } + + void printHelp() { + int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); + printVersion(); + // clang-format off + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filters use wildcards for matching strings\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "something passes a filter if any of the strings in a filter matches\n"; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; +#endif + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "Query flags - the program quits after them. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " + << Whitespace(sizePrefixDisplay*0) << "prints this message\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " + << Whitespace(sizePrefixDisplay*1) << "prints the version\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " + << Whitespace(sizePrefixDisplay*1) << "prints the number of matching tests\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " + << Whitespace(sizePrefixDisplay*1) << "lists all matching tests by name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " + << Whitespace(sizePrefixDisplay*1) << "lists all matching test suites\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " + << Whitespace(sizePrefixDisplay*1) << "lists all registered reporters\n\n"; + // ================================================================================== << 79 + s << Color::Cyan << "[doctest] " << Color::None; + s << "The available / options/filters are:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " + << Whitespace(sizePrefixDisplay*1) << "filters subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " + << Whitespace(sizePrefixDisplay*1) << "reporters to use (console is default)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " + << Whitespace(sizePrefixDisplay*1) << "output filename\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " + << Whitespace(sizePrefixDisplay*1) << "how the tests should be ordered\n"; + s << Whitespace(sizePrefixDisplay*3) << " - by [file/suite/name/rand]\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " + << Whitespace(sizePrefixDisplay*1) << "seed for random ordering\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " + << Whitespace(sizePrefixDisplay*1) << "the first test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " + << Whitespace(sizePrefixDisplay*1) << "the last test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " + << Whitespace(sizePrefixDisplay*1) << "stop after failed assertions\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " + << Whitespace(sizePrefixDisplay*1) << "apply filters for the first levels\n"; + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " + << Whitespace(sizePrefixDisplay*1) << "include successful assertions in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " + << Whitespace(sizePrefixDisplay*1) << "filters being treated as case sensitive\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " + << Whitespace(sizePrefixDisplay*1) << "exits after the tests finish\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " + << Whitespace(sizePrefixDisplay*1) << "prints the time duration of each test\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " + << Whitespace(sizePrefixDisplay*1) << "skips exceptions-related assert checks\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " + << Whitespace(sizePrefixDisplay*1) << "returns (or exits) always with success\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " + << Whitespace(sizePrefixDisplay*1) << "skips all runtime doctest operations\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework version in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " + << Whitespace(sizePrefixDisplay*1) << "disables colors in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " + << Whitespace(sizePrefixDisplay*1) << "use colors even when not in a tty\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " + << Whitespace(sizePrefixDisplay*1) << "disables breakpoints in debuggers\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " + << Whitespace(sizePrefixDisplay*1) << "don't skip test cases marked as skip\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " + << Whitespace(sizePrefixDisplay*1) << ":n: vs (n): for line numbers in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " + << Whitespace(sizePrefixDisplay*1) << "only filenames and no paths in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " + << Whitespace(sizePrefixDisplay*1) << "0 instead of real line numbers in output\n"; + // ================================================================================== << 79 + // clang-format on + + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "for more information visit the project documentation\n\n"; + } + + void printRegisteredReporters() { + printVersion(); + auto printReporters = [this] (const reporterMap& reporters, const char* type) { + if(reporters.size()) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; + for(auto& curr : reporters) + s << "priority: " << std::setw(5) << curr.first.first + << " name: " << curr.first.second << "\n"; + } + }; + printReporters(getListeners(), "listeners"); + printReporters(getReporters(), "reporters"); + } + + void list_query_results() { + separator_to_stream(); + if(opt.count || opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + } else if(opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " + << g_cs->numTestSuitesPassingFilters << "\n"; + } + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + if(opt.version) { + printVersion(); + } else if(opt.help) { + printHelp(); + } else if(opt.list_reporters) { + printRegisteredReporters(); + } else if(opt.count || opt.list_test_cases) { + if(opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None + << "listing all test case names\n"; + separator_to_stream(); + } + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_name << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + + } else if(opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; + separator_to_stream(); + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_test_suite << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " + << g_cs->numTestSuitesPassingFilters << "\n"; + } + } + + void test_run_start() override { printIntro(); } + + void test_run_end(const TestRunStats& p) override { + separator_to_stream(); + s << std::dec; + + const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; + s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(6) + << p.numTestCasesPassingFilters << " | " + << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : + Color::Green) + << std::setw(6) << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" + << Color::None << " | " << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) + << std::setw(6) << p.numTestCasesFailed << " failed" << Color::None << " | "; + if(opt.no_skipped_summary == false) { + const int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; + s << (numSkipped == 0 ? Color::None : Color::Yellow) << std::setw(6) << numSkipped + << " skipped" << Color::None; + } + s << "\n"; + s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(6) + << p.numAsserts << " | " + << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) + << std::setw(6) << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None + << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(6) + << p.numAssertsFailed << " failed" << Color::None << " |\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) + << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; + } + + void test_case_start(const TestCaseData& in) override { + hasLoggedCurrentTestStart = false; + tc = ∈ + } + + void test_case_reenter(const TestCaseData&) override {} + + void test_case_end(const CurrentTestCaseStats& st) override { + // log the preamble of the test case only if there is something + // else to print - something other than that an assert has failed + if(opt.duration || + (st.failure_flags && st.failure_flags != TestCaseFailureReason::AssertFailure)) + logTestStart(); + + if(opt.duration) + s << Color::None << std::setprecision(6) << std::fixed << st.seconds + << " s: " << tc->m_name << "\n"; + + if(st.failure_flags & TestCaseFailureReason::Timeout) + s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) + << std::fixed << tc->m_timeout << "!\n"; + + if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { + s << Color::Yellow << "Failed as expected so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { + s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures + << " times so marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { + s << Color::Yellow << "Failed exactly " << tc->m_expected_failures + << " times as expected so marking it as not failed!\n"; + } + if(st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + s << Color::Red << "Aborting - too many failed asserts!\n"; + } + s << Color::None; // lgtm [cpp/useless-expression] + } + + void test_case_exception(const TestCaseException& e) override { + logTestStart(); + + file_line_to_stream(s, tc->m_file, tc->m_line, " "); + successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : + assertType::is_check); + s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ") + << Color::Cyan << e.error_string << "\n"; + + int num_stringified_contexts = get_num_stringified_contexts(); + if(num_stringified_contexts) { + auto stringified_contexts = get_stringified_contexts(); + s << Color::None << " logged: "; + for(int i = num_stringified_contexts; i > 0; --i) { + s << (i == num_stringified_contexts ? "" : " ") + << stringified_contexts[i - 1] << "\n"; + } + } + s << "\n" << Color::None; + } + + void subcase_start(const SubcaseSignature& subc) override { + std::lock_guard lock(mutex); + subcasesStack.push_back(subc); + hasLoggedCurrentTestStart = false; + } + + void subcase_end() override { + std::lock_guard lock(mutex); + subcasesStack.pop_back(); + hasLoggedCurrentTestStart = false; + } + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed && !opt.success) + return; + + std::lock_guard lock(mutex); + + logTestStart(); + + file_line_to_stream(s, rb.m_file, rb.m_line, " "); + successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); + if((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == + 0) //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " + << Color::None; + + if(rb.m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; + } else if((rb.m_at & assertType::is_throws_as) && + (rb.m_at & assertType::is_throws_with)) { //!OCLINT + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string << "\", " << rb.m_exception_type << " ) " << Color::None; + if(rb.m_threw) { + if(!rb.m_failed) { + s << "threw as expected!\n"; + } else { + s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; + } + } else { + s << "did NOT throw at all!\n"; + } + } else if(rb.m_at & + assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", " + << rb.m_exception_type << " ) " << Color::None + << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & + assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string << "\" ) " << Color::None + << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan + << rb.m_exception << "\n"; + } else { + s << (rb.m_threw ? "THREW exception: " : + (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); + if(rb.m_threw) + s << rb.m_exception << "\n"; + else + s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; + } + + log_contexts(); + } + + void log_message(const MessageData& mb) override { + std::lock_guard lock(mutex); + + logTestStart(); + + file_line_to_stream(s, mb.m_file, mb.m_line, " "); + s << getSuccessOrFailColor(false, mb.m_severity) + << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, + "MESSAGE") << ": "; + s << Color::None << mb.m_string << "\n"; + log_contexts(); + } + + void test_case_skipped(const TestCaseData&) override {} + }; + + DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); + +#ifdef DOCTEST_PLATFORM_WINDOWS + struct DebugOutputWindowReporter : public ConsoleReporter + { + DOCTEST_THREAD_LOCAL static std::ostringstream oss; + + DebugOutputWindowReporter(const ContextOptions& co) + : ConsoleReporter(co, oss) {} + +#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ + void func(type arg) override { \ + bool with_col = g_no_colors; \ + g_no_colors = false; \ + ConsoleReporter::func(arg); \ + DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ + oss.str(""); \ + g_no_colors = with_col; \ + } + + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData&, in) + }; + + DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; +#endif // DOCTEST_PLATFORM_WINDOWS + + // the implementation of parseOption() + bool parseOptionImpl(int argc, const char* const* argv, const char* pattern, String* value) { + // going from the end to the beginning and stopping on the first occurrence from the end + for(int i = argc; i > 0; --i) { + auto index = i - 1; + auto temp = std::strstr(argv[index], pattern); + if(temp && (value || strlen(temp) == strlen(pattern))) { //!OCLINT prefer early exits and continue + // eliminate matches in which the chars before the option are not '-' + bool noBadCharsFound = true; + auto curr = argv[index]; + while(curr != temp) { + if(*curr++ != '-') { + noBadCharsFound = false; + break; + } + } + if(noBadCharsFound && argv[index][0] == '-') { + if(value) { + // parsing the value of an option + temp += strlen(pattern); + const unsigned len = strlen(temp); + if(len) { + *value = temp; + return true; + } + } else { + // just a flag - no value + return true; + } + } + } + } + return false; + } + + // parses an option and returns the string after the '=' character + bool parseOption(int argc, const char* const* argv, const char* pattern, String* value = nullptr, + const String& defaultVal = String()) { + if(value) + *value = defaultVal; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + // offset (normally 3 for "dt-") to skip prefix + if(parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) + return true; +#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + return parseOptionImpl(argc, argv, pattern, value); + } + + // locates a flag on the command line + bool parseFlag(int argc, const char* const* argv, const char* pattern) { + return parseOption(argc, argv, pattern); + } + + // parses a comma separated list of words after a pattern in one of the arguments in argv + bool parseCommaSepArgs(int argc, const char* const* argv, const char* pattern, + std::vector& res) { + String filtersString; + if(parseOption(argc, argv, pattern, &filtersString)) { + // tokenize with "," as a separator + // cppcheck-suppress strtokCalled + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + auto pch = std::strtok(filtersString.c_str(), ","); // modifies the string + while(pch != nullptr) { + if(strlen(pch)) + res.push_back(pch); + // uses the strtok() internal state to go to the next token + // cppcheck-suppress strtokCalled + pch = std::strtok(nullptr, ","); + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + return true; + } + return false; + } + + enum optionType + { + option_bool, + option_int + }; + + // parses an int/bool option from the command line + bool parseIntOption(int argc, const char* const* argv, const char* pattern, optionType type, + int& res) { + String parsedValue; + if(!parseOption(argc, argv, pattern, &parsedValue)) + return false; + + if(type == 0) { + // boolean + const char positive[][5] = {"1", "true", "on", "yes"}; // 5 - strlen("true") + 1 + const char negative[][6] = {"0", "false", "off", "no"}; // 6 - strlen("false") + 1 + + // if the value matches any of the positive/negative possibilities + for(unsigned i = 0; i < 4; i++) { + if(parsedValue.compare(positive[i], true) == 0) { + res = 1; //!OCLINT parameter reassignment + return true; + } + if(parsedValue.compare(negative[i], true) == 0) { + res = 0; //!OCLINT parameter reassignment + return true; + } + } + } else { + // integer + // TODO: change this to use std::stoi or something else! currently it uses undefined behavior - assumes '0' on failed parse... + int theInt = std::atoi(parsedValue.c_str()); // NOLINT + if(theInt != 0) { + res = theInt; //!OCLINT parameter reassignment + return true; + } + } + return false; + } +} // namespace + +Context::Context(int argc, const char* const* argv) + : p(new detail::ContextState) { + parseArgs(argc, argv, true); + if(argc) + p->binary_name = argv[0]; +} + +Context::~Context() { + if(g_cs == p) + g_cs = nullptr; + delete p; +} + +void Context::applyCommandLine(int argc, const char* const* argv) { + parseArgs(argc, argv); + if(argc) + p->binary_name = argv[0]; +} + +// parses args +void Context::parseArgs(int argc, const char* const* argv, bool withDefaults) { + using namespace detail; + + // clang-format off + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); + // clang-format on + + int intRes = 0; + String strRes; + +#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ + p->var = !!intRes; \ + else if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname)) \ + p->var = true; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ + p->var = intRes; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ + if(parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ + parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || \ + withDefaults) \ + p->var = strRes + + // clang-format off + DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); + DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); + DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); + + DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); + DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); + + DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); + DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); + + DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); + // clang-format on + + if(withDefaults) { + p->help = false; + p->version = false; + p->count = false; + p->list_test_cases = false; + p->list_test_suites = false; + p->list_reporters = false; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { + p->help = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { + p->version = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { + p->count = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { + p->list_test_cases = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { + p->list_test_suites = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { + p->list_reporters = true; + p->exit = true; + } +} + +// allows the user to add procedurally to the filters from the command line +void Context::addFilter(const char* filter, const char* value) { setOption(filter, value); } + +// allows the user to clear all filters from the command line +void Context::clearFilters() { + for(auto& curr : p->filters) + curr.clear(); +} + +// allows the user to override procedurally the int/bool options from the command line +void Context::setOption(const char* option, int value) { + setOption(option, toString(value).c_str()); +} + +// allows the user to override procedurally the string options from the command line +void Context::setOption(const char* option, const char* value) { + auto argv = String("-") + option + "=" + value; + auto lvalue = argv.c_str(); + parseArgs(1, &lvalue); +} + +// users should query this in their main() and exit the program if true +bool Context::shouldExit() { return p->exit; } + +void Context::setAsDefaultForAssertsOutOfTestCases() { g_cs = p; } + +void Context::setAssertHandler(detail::assert_handler ah) { p->ah = ah; } + +// the main function that does all the filtering and test running +int Context::run() { + using namespace detail; + + // save the old context state in case such was setup - for using asserts out of a testing context + auto old_cs = g_cs; + // this is the current contest + g_cs = p; + is_running_in_test = true; + + g_no_colors = p->no_colors; + p->resetRunData(); + + // stdout by default + p->cout = &std::cout; + p->cerr = &std::cerr; + + // or to a file if specified + std::fstream fstr; + if(p->out.size()) { + fstr.open(p->out.c_str(), std::fstream::out); + p->cout = &fstr; + } + + auto cleanup_and_return = [&]() { + if(fstr.is_open()) + fstr.close(); + + // restore context + g_cs = old_cs; + is_running_in_test = false; + + // we have to free the reporters which were allocated when the run started + for(auto& curr : p->reporters_currently_used) + delete curr; + p->reporters_currently_used.clear(); + + if(p->numTestCasesFailed && !p->no_exitcode) + return EXIT_FAILURE; + return EXIT_SUCCESS; + }; + + // setup default reporter if none is given through the command line + if(p->filters[8].empty()) + p->filters[8].push_back("console"); + + // check to see if any of the registered reporters has been selected + for(auto& curr : getReporters()) { + if(matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) + p->reporters_currently_used.push_back(curr.second(*g_cs)); + } + + // TODO: check if there is nothing in reporters_currently_used + + // prepend all listeners + for(auto& curr : getListeners()) + p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); + +#ifdef DOCTEST_PLATFORM_WINDOWS + if(isDebuggerActive()) + p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); +#endif // DOCTEST_PLATFORM_WINDOWS + + // handle version, help and no_run + if(p->no_run || p->version || p->help || p->list_reporters) { + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); + + return cleanup_and_return(); + } + + std::vector testArray; + for(auto& curr : getRegisteredTests()) + testArray.push_back(&curr); + p->numTestCases = testArray.size(); + + // sort the collected records + if(!testArray.empty()) { + if(p->order_by.compare("file", true) == 0) { + std::sort(testArray.begin(), testArray.end(), fileOrderComparator); + } else if(p->order_by.compare("suite", true) == 0) { + std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); + } else if(p->order_by.compare("name", true) == 0) { + std::sort(testArray.begin(), testArray.end(), nameOrderComparator); + } else if(p->order_by.compare("rand", true) == 0) { + std::srand(p->rand_seed); + + // random_shuffle implementation + const auto first = &testArray[0]; + for(size_t i = testArray.size() - 1; i > 0; --i) { + int idxToSwap = std::rand() % (i + 1); // NOLINT + + const auto temp = first[i]; + + first[i] = first[idxToSwap]; + first[idxToSwap] = temp; + } + } + } + + std::set testSuitesPassingFilt; + + bool query_mode = p->count || p->list_test_cases || p->list_test_suites; + std::vector queryResults; + + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); + + // invoke the registered functions if they match the filter criteria (or just count them) + for(auto& curr : testArray) { + const auto& tc = *curr; + + bool skip_me = false; + if(tc.m_skip && !p->no_skip) + skip_me = true; + + if(!matchesAny(tc.m_file, p->filters[0], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_file, p->filters[1], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) + skip_me = true; + + if(!skip_me) + p->numTestCasesPassingFilters++; + + // skip the test if it is not in the execution range + if((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || + (p->first > p->numTestCasesPassingFilters)) + skip_me = true; + + if(skip_me) { + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); + continue; + } + + // do not execute the test if we are to only count the number of filter passing tests + if(p->count) + continue; + + // print the name of the test and don't execute it + if(p->list_test_cases) { + queryResults.push_back(&tc); + continue; + } + + // print the name of the test suite if not done already and don't execute it + if(p->list_test_suites) { + if((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { + queryResults.push_back(&tc); + testSuitesPassingFilt.insert(tc.m_test_suite); + p->numTestSuitesPassingFilters++; + } + continue; + } + + // execute the test if it passes all the filtering + { + p->currentTest = &tc; + + p->failure_flags = TestCaseFailureReason::None; + p->seconds = 0; + + // reset atomic counters + p->numAssertsFailedCurrentTest_atomic = 0; + p->numAssertsCurrentTest_atomic = 0; + + p->subcasesPassed.clear(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); + + p->timer.start(); + + bool run_test = true; + + do { + // reset some of the fields for subcases (except for the set of fully passed ones) + p->should_reenter = false; + p->subcasesCurrentMaxLevel = 0; + p->subcasesStack.clear(); + + p->shouldLogCurrentException = true; + + // reset stuff for logging with INFO() + p->stringifiedContexts.clear(); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + FatalConditionHandler fatalConditionHandler; // Handle signals + // execute the test + tc.m_test(); + fatalConditionHandler.reset(); +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + } catch(const TestFailureException&) { + p->failure_flags |= TestCaseFailureReason::AssertFailure; + } catch(...) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, + {translateActiveException(), false}); + p->failure_flags |= TestCaseFailureReason::Exception; + } +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + + // exit this loop if enough assertions have failed - even if there are more subcases + if(p->abort_after > 0 && + p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { + run_test = false; + p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; + } + + if(p->should_reenter && run_test) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); + if(!p->should_reenter) + run_test = false; + } while(run_test); + + p->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + p->currentTest = nullptr; + + // stop executing tests if enough assertions have failed + if(p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) + break; + } + } + + if(!query_mode) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } else { + QueryData qdata; + qdata.run_stats = g_cs; + qdata.data = queryResults.data(); + qdata.num_data = unsigned(queryResults.size()); + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); + } + + // see these issues on the reasoning for this: + // - https://github.com/onqtam/doctest/issues/143#issuecomment-414418903 + // - https://github.com/onqtam/doctest/issues/126 + auto DOCTEST_FIX_FOR_MACOS_LIBCPP_IOSFWD_STRING_LINK_ERRORS = []() DOCTEST_NOINLINE + { std::cout << std::string(); }; + DOCTEST_FIX_FOR_MACOS_LIBCPP_IOSFWD_STRING_LINK_ERRORS(); + + return cleanup_and_return(); +} + +IReporter::~IReporter() = default; + +int IReporter::get_num_active_contexts() { return detail::g_infoContexts.size(); } +const IContextScope* const* IReporter::get_active_contexts() { + return get_num_active_contexts() ? &detail::g_infoContexts[0] : nullptr; +} + +int IReporter::get_num_stringified_contexts() { return detail::g_cs->stringifiedContexts.size(); } +const String* IReporter::get_stringified_contexts() { + return get_num_stringified_contexts() ? &detail::g_cs->stringifiedContexts[0] : nullptr; +} + +namespace detail { + void registerReporterImpl(const char* name, int priority, reporterCreatorFunc c, bool isReporter) { + if(isReporter) + getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + else + getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + } +} // namespace detail + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 +int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_LIBRARY_IMPLEMENTATION +#endif // DOCTEST_CONFIG_IMPLEMENT diff --git a/Firmware/doctest/parts/doctest_fwd.h b/Firmware/doctest/parts/doctest_fwd.h new file mode 100644 index 00000000..031f2cd1 --- /dev/null +++ b/Firmware/doctest/parts/doctest_fwd.h @@ -0,0 +1,2604 @@ +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2019 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying file LICENSE.txt or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/onqtam/doctest/blob/master/doc/markdown/readme.md +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= +// +// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt +// +// The concept of subcases (sections in Catch) and expression decomposition are from there. +// Some parts of the code are taken directly: +// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> +// - the Approx() helper class for floating point comparison +// - colors in the console +// - breaking into a debugger +// - signal / SEH handling +// - timer +// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) +// +// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= + +#ifndef DOCTEST_LIBRARY_INCLUDED +#define DOCTEST_LIBRARY_INCLUDED + +// ================================================================================================= +// == VERSION ====================================================================================== +// ================================================================================================= + +#define DOCTEST_VERSION_MAJOR 2 +#define DOCTEST_VERSION_MINOR 3 +#define DOCTEST_VERSION_PATCH 7 +#define DOCTEST_VERSION_STR "2.3.7" + +#define DOCTEST_VERSION \ + (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) + +// ================================================================================================= +// == COMPILER VERSION ============================================================================= +// ================================================================================================= + +// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect + +#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR)*10000000 + (MINOR)*100000 + (PATCH)) + +// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... +#if defined(_MSC_VER) && defined(_MSC_FULL_VER) +#if _MSC_VER == _MSC_FULL_VER / 10000 +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) +#else // MSVC +#define DOCTEST_MSVC \ + DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) +#endif // MSVC +#endif // MSVC +#if defined(__clang__) && defined(__clang_minor__) +#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && \ + !defined(__INTEL_COMPILER) +#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#endif // GCC + +#ifndef DOCTEST_MSVC +#define DOCTEST_MSVC 0 +#endif // DOCTEST_MSVC +#ifndef DOCTEST_CLANG +#define DOCTEST_CLANG 0 +#endif // DOCTEST_CLANG +#ifndef DOCTEST_GCC +#define DOCTEST_GCC 0 +#endif // DOCTEST_GCC + +// ================================================================================================= +// == COMPILER WARNINGS HELPERS ==================================================================== +// ================================================================================================= + +#if DOCTEST_CLANG +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) +#else // DOCTEST_CLANG +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_CLANG + +#if DOCTEST_GCC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") +#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) +#else // DOCTEST_GCC +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH +#define DOCTEST_GCC_SUPPRESS_WARNING(w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_GCC + +#if DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) +#else // DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_MSVC + +// ================================================================================================= +// == COMPILER WARNINGS ============================================================================ +// ================================================================================================= + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-local-typedef") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") +DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") +DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") +DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-local-typedefs") +DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4616) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4619) // invalid compiler warning +DOCTEST_MSVC_SUPPRESS_WARNING(4996) // The compiler encountered a deprecated declaration +DOCTEST_MSVC_SUPPRESS_WARNING(4706) // assignment within conditional expression +DOCTEST_MSVC_SUPPRESS_WARNING(4512) // 'class' : assignment operator could not be generated +DOCTEST_MSVC_SUPPRESS_WARNING(4127) // conditional expression is constant +DOCTEST_MSVC_SUPPRESS_WARNING(4820) // padding +DOCTEST_MSVC_SUPPRESS_WARNING(4625) // copy constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4626) // assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5027) // move assignment operator was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(5026) // move constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly defined as deleted +DOCTEST_MSVC_SUPPRESS_WARNING(4640) // construction of local static object is not thread-safe +// static analysis +DOCTEST_MSVC_SUPPRESS_WARNING(26439) // This kind of function may not throw. Declare it 'noexcept' +DOCTEST_MSVC_SUPPRESS_WARNING(26495) // Always initialize a member variable +DOCTEST_MSVC_SUPPRESS_WARNING(26451) // Arithmetic overflow ... +DOCTEST_MSVC_SUPPRESS_WARNING(26444) // Avoid unnamed objects with custom construction and dtr... +DOCTEST_MSVC_SUPPRESS_WARNING(26812) // Prefer 'enum class' over 'enum' + +// 4548 - expression before comma has no effect; expected expression with side - effect +// 4265 - class has virtual functions, but destructor is not virtual +// 4986 - exception specification does not match previous declaration +// 4350 - behavior change: 'member1' called instead of 'member2' +// 4668 - 'x' is not defined as a preprocessor macro, replacing with '0' for '#if/#elif' +// 4365 - conversion from 'int' to 'unsigned long', signed/unsigned mismatch +// 4774 - format string expected in argument 'x' is not a string literal +// 4820 - padding in structs + +// only 4 should be disabled globally: +// - 4514 # unreferenced inline function has been removed +// - 4571 # SEH related +// - 4710 # function not inlined +// - 4711 # function 'x' selected for automatic inline expansion + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + DOCTEST_MSVC_SUPPRESS_WARNING(4548) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4265) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4986) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4350) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4668) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) \ + DOCTEST_MSVC_SUPPRESS_WARNING(5105) + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP + +// ================================================================================================= +// == FEATURE DETECTION ============================================================================ +// ================================================================================================= + +// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support +// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx +// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html +// MSVC version table: +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) +// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) +// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) +// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) +// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) +// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) +// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) +// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) + +#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#define DOCTEST_CONFIG_WINDOWS_SEH +#endif // MSVC +#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) +#undef DOCTEST_CONFIG_WINDOWS_SEH +#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH + +#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && \ + !defined(__EMSCRIPTEN__) +#define DOCTEST_CONFIG_POSIX_SIGNALS +#endif // _WIN32 +#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) +#undef DOCTEST_CONFIG_POSIX_SIGNALS +#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // no exceptions +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) +#define DOCTEST_CONFIG_IMPLEMENT +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#if defined(_WIN32) || defined(__CYGWIN__) +#if DOCTEST_MSVC +#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) +#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) +#else // MSVC +#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) +#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) +#endif // MSVC +#else // _WIN32 +#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define DOCTEST_SYMBOL_IMPORT +#endif // _WIN32 + +#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#ifdef DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT +#else // DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT +#endif // DOCTEST_CONFIG_IMPLEMENT +#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#define DOCTEST_INTERFACE +#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL + +#define DOCTEST_EMPTY + +#if DOCTEST_MSVC +#define DOCTEST_NOINLINE __declspec(noinline) +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#else // MSVC +#define DOCTEST_NOINLINE __attribute__((noinline)) +#define DOCTEST_UNUSED __attribute__((unused)) +#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) +#endif // MSVC + +// ================================================================================================= +// == FEATURE DETECTION END ======================================================================== +// ================================================================================================= + +// internal macros for string concatenation and anonymous variable name generation +#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 +#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) +#ifdef __COUNTER__ // not standard and may be missing for some compilers +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __COUNTER__) +#else // __COUNTER__ +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __LINE__) +#endif // __COUNTER__ + +#define DOCTEST_TOSTR(x) #x + +#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x& +#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x +#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE + +// not using __APPLE__ because... this is how Catch does it +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED +#define DOCTEST_PLATFORM_MAC +#elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED) +#define DOCTEST_PLATFORM_IPHONE +#elif defined(_WIN32) +#define DOCTEST_PLATFORM_WINDOWS +#else // DOCTEST_PLATFORM +#define DOCTEST_PLATFORM_LINUX +#endif // DOCTEST_PLATFORM + +#define DOCTEST_GLOBAL_NO_WARNINGS(var) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-variable") \ + static int var DOCTEST_UNUSED // NOLINT(fuchsia-statically-constructed-objects,cert-err58-cpp) +#define DOCTEST_GLOBAL_NO_WARNINGS_END() DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_BREAK_INTO_DEBUGGER +// should probably take a look at https://github.com/scottt/debugbreak +#ifdef DOCTEST_PLATFORM_MAC +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) +#elif DOCTEST_MSVC +#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() +#elif defined(__MINGW32__) +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") +extern "C" __declspec(dllimport) void __stdcall DebugBreak(); +DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() +#else // linux +#define DOCTEST_BREAK_INTO_DEBUGGER() ((void)0) +#endif // linux +#endif // DOCTEST_BREAK_INTO_DEBUGGER + +// this is kept here for backwards compatibility since the config option was changed +#ifdef DOCTEST_CONFIG_USE_IOSFWD +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif // DOCTEST_CONFIG_USE_IOSFWD + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#include +#include +#include +#else // DOCTEST_CONFIG_USE_STD_HEADERS + +#if DOCTEST_CLANG +// to detect if libc++ is being used with clang (the _LIBCPP_VERSION identifier) +#include +#endif // clang + +#ifdef _LIBCPP_VERSION +#define DOCTEST_STD_NAMESPACE_BEGIN _LIBCPP_BEGIN_NAMESPACE_STD +#define DOCTEST_STD_NAMESPACE_END _LIBCPP_END_NAMESPACE_STD +#else // _LIBCPP_VERSION +#define DOCTEST_STD_NAMESPACE_BEGIN namespace std { +#define DOCTEST_STD_NAMESPACE_END } +#endif // _LIBCPP_VERSION + +// Forward declaring 'X' in namespace std is not permitted by the C++ Standard. +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643) + +DOCTEST_STD_NAMESPACE_BEGIN // NOLINT (cert-dcl58-cpp) +typedef decltype(nullptr) nullptr_t; +template +struct char_traits; +template <> +struct char_traits; +template +class basic_ostream; +typedef basic_ostream> ostream; +template +class tuple; +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/onqtam/doctest/issues/183 +template +class allocator; +template +class basic_string; +using string = basic_string, allocator>; +#endif // VS 2019 +DOCTEST_STD_NAMESPACE_END + +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#include +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + +namespace doctest { + +DOCTEST_INTERFACE extern bool is_running_in_test; + +// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings with length +// of up to 23 chars on the stack before going on the heap - the last byte of the buffer is used for: +// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) +// - if small - capacity left before going on the heap - using the lowest 5 bits +// - if small - 2 bits are left unused - the second and third highest ones +// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) +// and the "is small" bit remains "0" ("as well as the capacity left") so its OK +// Idea taken from this lecture about the string implementation of facebook/folly - fbstring +// https://www.youtube.com/watch?v=kPR8h4-qZdk +// TODO: +// - optimizations - like not deleting memory unnecessarily in operator= and etc. +// - resize/reserve/clear +// - substr +// - replace +// - back/front +// - iterator stuff +// - find & friends +// - push_back/pop_back +// - assign/insert/erase +// - relational operators as free functions - taking const char* as one of the params +class DOCTEST_INTERFACE String +{ + static const unsigned len = 24; //!OCLINT avoid private static members + static const unsigned last = len - 1; //!OCLINT avoid private static members + + struct view // len should be more than sizeof(view) - because of the final byte for flags + { + char* ptr; + unsigned size; + unsigned capacity; + }; + + union + { + char buf[len]; + view data; + }; + + bool isOnStack() const { return (buf[last] & 128) == 0; } + void setOnHeap(); + void setLast(unsigned in = last); + + void copy(const String& other); + +public: + String(); + ~String(); + + // cppcheck-suppress noExplicitConstructor + String(const char* in); + String(const char* in, unsigned in_size); + + String(const String& other); + String& operator=(const String& other); + + String& operator+=(const String& other); + String operator+(const String& other) const; + + String(String&& other); + String& operator=(String&& other); + + char operator[](unsigned i) const; + char& operator[](unsigned i); + + // the only functions I'm willing to leave in the interface - available for inlining + const char* c_str() const { return const_cast(this)->c_str(); } // NOLINT + char* c_str() { + if(isOnStack()) + return reinterpret_cast(buf); + return data.ptr; + } + + unsigned size() const; + unsigned capacity() const; + + int compare(const char* other, bool no_case = false) const; + int compare(const String& other, bool no_case = false) const; +}; + +DOCTEST_INTERFACE bool operator==(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator!=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs); + +DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in); + +namespace Color { + enum Enum + { + None = 0, + White, + Red, + Green, + Blue, + Cyan, + Yellow, + Grey, + + Bright = 0x10, + + BrightRed = Bright | Red, + BrightGreen = Bright | Green, + LightGrey = Bright | Grey, + BrightWhite = Bright | White + }; + + DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, Color::Enum code); +} // namespace Color + +namespace assertType { + enum Enum + { + // macro traits + + is_warn = 1, + is_check = 2 * is_warn, + is_require = 2 * is_check, + + is_normal = 2 * is_require, + is_throws = 2 * is_normal, + is_throws_as = 2 * is_throws, + is_throws_with = 2 * is_throws_as, + is_nothrow = 2 * is_throws_with, + + is_false = 2 * is_nothrow, + is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types + + is_eq = 2 * is_unary, + is_ne = 2 * is_eq, + + is_lt = 2 * is_ne, + is_gt = 2 * is_lt, + + is_ge = 2 * is_gt, + is_le = 2 * is_ge, + + // macro types + + DT_WARN = is_normal | is_warn, + DT_CHECK = is_normal | is_check, + DT_REQUIRE = is_normal | is_require, + + DT_WARN_FALSE = is_normal | is_false | is_warn, + DT_CHECK_FALSE = is_normal | is_false | is_check, + DT_REQUIRE_FALSE = is_normal | is_false | is_require, + + DT_WARN_THROWS = is_throws | is_warn, + DT_CHECK_THROWS = is_throws | is_check, + DT_REQUIRE_THROWS = is_throws | is_require, + + DT_WARN_THROWS_AS = is_throws_as | is_warn, + DT_CHECK_THROWS_AS = is_throws_as | is_check, + DT_REQUIRE_THROWS_AS = is_throws_as | is_require, + + DT_WARN_THROWS_WITH = is_throws_with | is_warn, + DT_CHECK_THROWS_WITH = is_throws_with | is_check, + DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, + + DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, + DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, + DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, + + DT_WARN_NOTHROW = is_nothrow | is_warn, + DT_CHECK_NOTHROW = is_nothrow | is_check, + DT_REQUIRE_NOTHROW = is_nothrow | is_require, + + DT_WARN_EQ = is_normal | is_eq | is_warn, + DT_CHECK_EQ = is_normal | is_eq | is_check, + DT_REQUIRE_EQ = is_normal | is_eq | is_require, + + DT_WARN_NE = is_normal | is_ne | is_warn, + DT_CHECK_NE = is_normal | is_ne | is_check, + DT_REQUIRE_NE = is_normal | is_ne | is_require, + + DT_WARN_GT = is_normal | is_gt | is_warn, + DT_CHECK_GT = is_normal | is_gt | is_check, + DT_REQUIRE_GT = is_normal | is_gt | is_require, + + DT_WARN_LT = is_normal | is_lt | is_warn, + DT_CHECK_LT = is_normal | is_lt | is_check, + DT_REQUIRE_LT = is_normal | is_lt | is_require, + + DT_WARN_GE = is_normal | is_ge | is_warn, + DT_CHECK_GE = is_normal | is_ge | is_check, + DT_REQUIRE_GE = is_normal | is_ge | is_require, + + DT_WARN_LE = is_normal | is_le | is_warn, + DT_CHECK_LE = is_normal | is_le | is_check, + DT_REQUIRE_LE = is_normal | is_le | is_require, + + DT_WARN_UNARY = is_normal | is_unary | is_warn, + DT_CHECK_UNARY = is_normal | is_unary | is_check, + DT_REQUIRE_UNARY = is_normal | is_unary | is_require, + + DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, + DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, + DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, + }; +} // namespace assertType + +DOCTEST_INTERFACE const char* assertString(assertType::Enum at); +DOCTEST_INTERFACE const char* failureString(assertType::Enum at); +DOCTEST_INTERFACE const char* skipPathFromFilename(const char* file); + +struct DOCTEST_INTERFACE TestCaseData +{ + const char* m_file; // the file in which the test was registered + unsigned m_line; // the line where the test was registered + const char* m_name; // name of the test case + const char* m_test_suite; // the test suite in which the test was added + const char* m_description; + bool m_skip; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; +}; + +struct DOCTEST_INTERFACE AssertData +{ + // common - for all asserts + const TestCaseData* m_test_case; + assertType::Enum m_at; + const char* m_file; + int m_line; + const char* m_expr; + bool m_failed; + + // exception-related - for all asserts + bool m_threw; + String m_exception; + + // for normal asserts + String m_decomp; + + // for specific exception-related asserts + bool m_threw_as; + const char* m_exception_type; + const char* m_exception_string; +}; + +struct DOCTEST_INTERFACE MessageData +{ + String m_string; + const char* m_file; + int m_line; + assertType::Enum m_severity; +}; + +struct DOCTEST_INTERFACE SubcaseSignature +{ + String m_name; + const char* m_file; + int m_line; + + bool operator<(const SubcaseSignature& other) const; +}; + +struct DOCTEST_INTERFACE IContextScope +{ + IContextScope(); + virtual ~IContextScope(); + virtual void stringify(std::ostream*) const = 0; +}; + +struct ContextOptions //!OCLINT too many fields +{ + std::ostream* cout; // stdout stream - std::cout by default + std::ostream* cerr; // stderr stream - std::cerr by default + String binary_name; // the test binary name + + // == parameters from the command line + String out; // output filename + String order_by; // how tests should be ordered + unsigned rand_seed; // the seed for rand ordering + + unsigned first; // the first (matching) test to be executed + unsigned last; // the last (matching) test to be executed + + int abort_after; // stop tests after this many failed assertions + int subcase_filter_levels; // apply the subcase filters for the first N levels + + bool success; // include successful assertions in output + bool case_sensitive; // if filtering should be case sensitive + bool exit; // if the program should be exited after the tests are ran/whatever + bool duration; // print the time duration of each test case + bool no_throw; // to skip exceptions-related assertion macros + bool no_exitcode; // if the framework should return 0 as the exitcode + bool no_run; // to not run the tests at all (can be done with an "*" exclude) + bool no_version; // to not print the version of the framework + bool no_colors; // if output to the console should be colorized + bool force_colors; // forces the use of colors even when a tty cannot be detected + bool no_breaks; // to not break into the debugger + bool no_skip; // don't skip test cases which are marked to be skipped + bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): + bool no_path_in_filenames; // if the path to files should be removed from the output + bool no_line_numbers; // if source code line numbers should be omitted from the output + bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! + + bool help; // to print the help + bool version; // to print the version + bool count; // if only the count of matching tests is to be retrieved + bool list_test_cases; // to list all tests matching the filters + bool list_test_suites; // to list all suites matching the filters + bool list_reporters; // lists all registered reporters +}; + +namespace detail { +#if defined(DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING) || defined(DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS) + template + struct enable_if + {}; + + template + struct enable_if + { typedef TYPE type; }; +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING) || DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + template struct remove_reference { typedef T type; }; + template struct remove_reference { typedef T type; }; + template struct remove_reference { typedef T type; }; + + template struct remove_const { typedef T type; }; + template struct remove_const { typedef T type; }; + // clang-format on + + template + struct deferred_false + // cppcheck-suppress unusedStructMember + { static const bool value = false; }; + + namespace has_insertion_operator_impl { + typedef char no; + typedef char yes[2]; + + struct any_t + { + template + // cppcheck-suppress noExplicitConstructor + any_t(const DOCTEST_REF_WRAP(T)); + }; + + yes& testStreamable(std::ostream&); + no testStreamable(no); + + no operator<<(const std::ostream&, const any_t&); + + template + struct has_insertion_operator + { + static std::ostream& s; + static const DOCTEST_REF_WRAP(T) t; + static const bool value = sizeof(decltype(testStreamable(s << t))) == sizeof(yes); + }; + } // namespace has_insertion_operator_impl + + template + struct has_insertion_operator : has_insertion_operator_impl::has_insertion_operator + {}; + + DOCTEST_INTERFACE void my_memcpy(void* dest, const void* src, unsigned num); + + DOCTEST_INTERFACE std::ostream* getTlsOss(); // returns a thread-local ostringstream + DOCTEST_INTERFACE String getTlsOssResult(); + + template + struct StringMakerBase + { + template + static String convert(const DOCTEST_REF_WRAP(T)) { + return "{?}"; + } + }; + + template <> + struct StringMakerBase + { + template + static String convert(const DOCTEST_REF_WRAP(T) in) { + *getTlsOss() << in; + return getTlsOssResult(); + } + }; + + DOCTEST_INTERFACE String rawMemoryToString(const void* object, unsigned size); + + template + String rawMemoryToString(const DOCTEST_REF_WRAP(T) object) { + return rawMemoryToString(&object, sizeof(object)); + } + + template + const char* type_to_string() { + return "<>"; + } +} // namespace detail + +template +struct StringMaker : public detail::StringMakerBase::value> +{}; + +template +struct StringMaker +{ + template + static String convert(U* p) { + if(p) + return detail::rawMemoryToString(p); + return "NULL"; + } +}; + +template +struct StringMaker +{ + static String convert(R C::*p) { + if(p) + return detail::rawMemoryToString(p); + return "NULL"; + } +}; + +template +String toString(const DOCTEST_REF_WRAP(T) value) { + return StringMaker::convert(value); +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(char* in); +DOCTEST_INTERFACE String toString(const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(bool in); +DOCTEST_INTERFACE String toString(float in); +DOCTEST_INTERFACE String toString(double in); +DOCTEST_INTERFACE String toString(double long in); + +DOCTEST_INTERFACE String toString(char in); +DOCTEST_INTERFACE String toString(char signed in); +DOCTEST_INTERFACE String toString(char unsigned in); +DOCTEST_INTERFACE String toString(int short in); +DOCTEST_INTERFACE String toString(int short unsigned in); +DOCTEST_INTERFACE String toString(int in); +DOCTEST_INTERFACE String toString(int unsigned in); +DOCTEST_INTERFACE String toString(int long in); +DOCTEST_INTERFACE String toString(int long unsigned in); +DOCTEST_INTERFACE String toString(int long long in); +DOCTEST_INTERFACE String toString(int long long unsigned in); +DOCTEST_INTERFACE String toString(std::nullptr_t in); + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/onqtam/doctest/issues/183 +DOCTEST_INTERFACE String toString(const std::string& in); +#endif // VS 2019 + +class DOCTEST_INTERFACE Approx +{ +public: + explicit Approx(double value); + + Approx operator()(double value) const; + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + explicit Approx(const T& value, + typename detail::enable_if::value>::type* = + static_cast(nullptr)) { + *this = Approx(static_cast(value)); + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& epsilon(double newEpsilon); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename detail::enable_if::value, Approx&>::type epsilon( + const T& newEpsilon) { + m_epsilon = static_cast(newEpsilon); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& scale(double newScale); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename detail::enable_if::value, Approx&>::type scale( + const T& newScale) { + m_scale = static_cast(newScale); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator==(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator!=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator<=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator>=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator< (const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator> (const Approx & lhs, double rhs); + + DOCTEST_INTERFACE friend String toString(const Approx& in); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_APPROX_PREFIX \ + template friend typename detail::enable_if::value, bool>::type + + DOCTEST_APPROX_PREFIX operator==(const T& lhs, const Approx& rhs) { return operator==(double(lhs), rhs); } + DOCTEST_APPROX_PREFIX operator==(const Approx& lhs, const T& rhs) { return operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator!=(const T& lhs, const Approx& rhs) { return !operator==(lhs, rhs); } + DOCTEST_APPROX_PREFIX operator!=(const Approx& lhs, const T& rhs) { return !operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator<=(const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator<=(const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator< (const T& lhs, const Approx& rhs) { return double(lhs) < rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator< (const Approx& lhs, const T& rhs) { return lhs.m_value < double(rhs) && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const T& lhs, const Approx& rhs) { return double(lhs) > rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const Approx& lhs, const T& rhs) { return lhs.m_value > double(rhs) && lhs != rhs; } +#undef DOCTEST_APPROX_PREFIX +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format on + +private: + double m_epsilon; + double m_scale; + double m_value; +}; + +DOCTEST_INTERFACE String toString(const Approx& in); + +DOCTEST_INTERFACE const ContextOptions* getContextOptions(); + +#if !defined(DOCTEST_CONFIG_DISABLE) + +namespace detail { + // clang-format off +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + template struct decay_array { typedef T type; }; + template struct decay_array { typedef T* type; }; + template struct decay_array { typedef T* type; }; + + template struct not_char_pointer { enum { value = 1 }; }; + template<> struct not_char_pointer { enum { value = 0 }; }; + template<> struct not_char_pointer { enum { value = 0 }; }; + + template struct can_use_op : public not_char_pointer::type> {}; +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + + struct DOCTEST_INTERFACE TestFailureException + { + }; + + DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + [[noreturn]] +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_INTERFACE void throwException(); + + struct DOCTEST_INTERFACE Subcase + { + SubcaseSignature m_signature; + bool m_entered = false; + + Subcase(const String& name, const char* file, int line); + ~Subcase(); + + operator bool() const; + }; + + template + String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char* op, + const DOCTEST_REF_WRAP(R) rhs) { + return toString(lhs) + op + toString(rhs); + } + +#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ + template \ + DOCTEST_NOINLINE Result operator op(const DOCTEST_REF_WRAP(R) rhs) { \ + bool res = op_macro(lhs, rhs); \ + if(m_at & assertType::is_false) \ + res = !res; \ + if(!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } + + // more checks could be added - like in Catch: + // https://github.com/catchorg/Catch2/pull/1480/files + // https://github.com/catchorg/Catch2/pull/1481/files +#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ + template \ + rt& operator op(const R&) { \ + static_assert(deferred_false::value, \ + "Expression Too Complex Please Rewrite As Binary Comparison!"); \ + return *this; \ + } + + struct DOCTEST_INTERFACE Result + { + bool m_passed; + String m_decomp; + + Result(bool passed, const String& decomposition = String()); + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Result, &) + DOCTEST_FORBIT_EXPRESSION(Result, ^) + DOCTEST_FORBIT_EXPRESSION(Result, |) + DOCTEST_FORBIT_EXPRESSION(Result, &&) + DOCTEST_FORBIT_EXPRESSION(Result, ||) + DOCTEST_FORBIT_EXPRESSION(Result, ==) + DOCTEST_FORBIT_EXPRESSION(Result, !=) + DOCTEST_FORBIT_EXPRESSION(Result, <) + DOCTEST_FORBIT_EXPRESSION(Result, >) + DOCTEST_FORBIT_EXPRESSION(Result, <=) + DOCTEST_FORBIT_EXPRESSION(Result, >=) + DOCTEST_FORBIT_EXPRESSION(Result, =) + DOCTEST_FORBIT_EXPRESSION(Result, +=) + DOCTEST_FORBIT_EXPRESSION(Result, -=) + DOCTEST_FORBIT_EXPRESSION(Result, *=) + DOCTEST_FORBIT_EXPRESSION(Result, /=) + DOCTEST_FORBIT_EXPRESSION(Result, %=) + DOCTEST_FORBIT_EXPRESSION(Result, <<=) + DOCTEST_FORBIT_EXPRESSION(Result, >>=) + DOCTEST_FORBIT_EXPRESSION(Result, &=) + DOCTEST_FORBIT_EXPRESSION(Result, ^=) + DOCTEST_FORBIT_EXPRESSION(Result, |=) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_GCC_SUPPRESS_WARNING_PUSH + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH + // https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 + DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch + //DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in operation + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + // clang-format off +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE bool +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE typename enable_if::value || can_use_op::value, bool>::type + inline bool eq(const char* lhs, const char* rhs) { return String(lhs) == String(rhs); } + inline bool ne(const char* lhs, const char* rhs) { return String(lhs) != String(rhs); } + inline bool lt(const char* lhs, const char* rhs) { return String(lhs) < String(rhs); } + inline bool gt(const char* lhs, const char* rhs) { return String(lhs) > String(rhs); } + inline bool le(const char* lhs, const char* rhs) { return String(lhs) <= String(rhs); } + inline bool ge(const char* lhs, const char* rhs) { return String(lhs) >= String(rhs); } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, \ + const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) l == r +#define DOCTEST_CMP_NE(l, r) l != r +#define DOCTEST_CMP_GT(l, r) l > r +#define DOCTEST_CMP_LT(l, r) l < r +#define DOCTEST_CMP_GE(l, r) l >= r +#define DOCTEST_CMP_LE(l, r) l <= r +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) eq(l, r) +#define DOCTEST_CMP_NE(l, r) ne(l, r) +#define DOCTEST_CMP_GT(l, r) gt(l, r) +#define DOCTEST_CMP_LT(l, r) lt(l, r) +#define DOCTEST_CMP_GE(l, r) ge(l, r) +#define DOCTEST_CMP_LE(l, r) le(l, r) +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + + template + // cppcheck-suppress copyCtorAndEqOperator + struct Expression_lhs + { + L lhs; + assertType::Enum m_at; + + explicit Expression_lhs(L in, assertType::Enum at) + : lhs(in) + , m_at(at) {} + + DOCTEST_NOINLINE operator Result() { + bool res = !!lhs; + if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional + res = !res; + + if(!res || getContextOptions()->success) + return Result(res, toString(lhs)); + return Result(res); + } + + // clang-format off + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) //!OCLINT bitwise operator in conditional + // clang-format on + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) + // these 2 are unfortunate because they should be allowed - they have higher precedence over the comparisons, but the + // ExpressionDecomposer class uses the left shift operator to capture the left operand of the binary expression... + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + struct DOCTEST_INTERFACE ExpressionDecomposer + { + assertType::Enum m_at; + + ExpressionDecomposer(assertType::Enum at); + + // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator precedence table) + // but then there will be warnings from GCC about "-Wparentheses" and since "_Pragma()" is problematic this will stay for now... + // https://github.com/catchorg/Catch2/issues/870 + // https://github.com/catchorg/Catch2/issues/565 + template + Expression_lhs operator<<(const DOCTEST_REF_WRAP(L) operand) { + return Expression_lhs(operand, m_at); + } + }; + + struct DOCTEST_INTERFACE TestSuite + { + const char* m_test_suite; + const char* m_description; + bool m_skip; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; + + TestSuite& operator*(const char* in); + + template + TestSuite& operator*(const T& in) { + in.fill(*this); + return *this; + } + }; + + typedef void (*funcType)(); + + struct DOCTEST_INTERFACE TestCase : public TestCaseData + { + funcType m_test; // a function pointer to the test case + + const char* m_type; // for templated test cases - gets appended to the real name + int m_template_id; // an ID used to distinguish between the different versions of a templated test case + String m_full_name; // contains the name (only for templated test cases!) + the template type + + TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const char* type = "", int template_id = -1); + + TestCase(const TestCase& other); + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase& operator=(const TestCase& other); + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& operator*(const char* in); + + template + TestCase& operator*(const T& in) { + in.fill(*this); + return *this; + } + + bool operator<(const TestCase& other) const; + }; + + // forward declarations of functions used by the macros + DOCTEST_INTERFACE int regTest(const TestCase& tc); + DOCTEST_INTERFACE int setTestSuite(const TestSuite& ts); + DOCTEST_INTERFACE bool isDebuggerActive(); + + template + int instantiationHelper(const T&) { return 0; } + + namespace binaryAssertComparison { + enum Enum + { + eq = 0, + ne, + gt, + lt, + ge, + le + }; + } // namespace binaryAssertComparison + + // clang-format off + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; + +#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; + // clang-format on + + DOCTEST_BINARY_RELATIONAL_OP(0, eq) + DOCTEST_BINARY_RELATIONAL_OP(1, ne) + DOCTEST_BINARY_RELATIONAL_OP(2, gt) + DOCTEST_BINARY_RELATIONAL_OP(3, lt) + DOCTEST_BINARY_RELATIONAL_OP(4, ge) + DOCTEST_BINARY_RELATIONAL_OP(5, le) + + struct DOCTEST_INTERFACE ResultBuilder : public AssertData + { + ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type = "", const char* exception_string = ""); + + void setResult(const Result& res); + + template + DOCTEST_NOINLINE void binary_assert(const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + m_failed = !RelationalComparator()(lhs, rhs); + if(m_failed || getContextOptions()->success) + m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); + } + + template + DOCTEST_NOINLINE void unary_assert(const DOCTEST_REF_WRAP(L) val) { + m_failed = !val; + + if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional + m_failed = !m_failed; + + if(m_failed || getContextOptions()->success) + m_decomp = toString(val); + } + + void translateException(); + + bool log(); + void react() const; + }; + + namespace assertAction { + enum Enum + { + nothing = 0, + dbgbreak = 1, + shouldthrow = 2 + }; + } // namespace assertAction + + DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData& ad); + + DOCTEST_INTERFACE void decomp_assert(assertType::Enum at, const char* file, int line, + const char* expr, Result result); + +#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ + do { \ + if(!is_running_in_test) { \ + if(failed) { \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + rb.m_decomp = decomp; \ + failed_out_of_a_testing_context(rb); \ + if(isDebuggerActive() && !getContextOptions()->no_breaks) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(checkIfShouldThrow(at)) \ + throwException(); \ + } \ + return; \ + } \ + } while(false) + +#define DOCTEST_ASSERT_IN_TESTS(decomp) \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + if(rb.m_failed || getContextOptions()->success) \ + rb.m_decomp = decomp; \ + if(rb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(rb.m_failed && checkIfShouldThrow(at)) \ + throwException() + + template + DOCTEST_NOINLINE void binary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + bool failed = !RelationalComparator()(lhs, rhs); + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + } + + template + DOCTEST_NOINLINE void unary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) val) { + bool failed = !val; + + if(at & assertType::is_false) //!OCLINT bitwise operator in conditional + failed = !failed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(toString(val)); + DOCTEST_ASSERT_IN_TESTS(toString(val)); + } + + struct DOCTEST_INTERFACE IExceptionTranslator + { + IExceptionTranslator(); + virtual ~IExceptionTranslator(); + virtual bool translate(String&) const = 0; + }; + + template + class ExceptionTranslator : public IExceptionTranslator //!OCLINT destructor of virtual class + { + public: + explicit ExceptionTranslator(String (*translateFunction)(T)) + : m_translateFunction(translateFunction) {} + + bool translate(String& res) const override { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { + throw; // lgtm [cpp/rethrow-no-exception] + // cppcheck-suppress catchExceptionByValue + } catch(T ex) { // NOLINT + res = m_translateFunction(ex); //!OCLINT parameter reassignment + return true; + } catch(...) {} //!OCLINT - empty catch statement +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + ((void)res); // to silence -Wunused-parameter + return false; + } + + private: + String (*m_translateFunction)(T); + }; + + DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator* et); + + template + struct StringStreamBase + { + template + static void convert(std::ostream* s, const T& in) { + *s << toString(in); + } + + // always treat char* as a string in this context - no matter + // if DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING is defined + static void convert(std::ostream* s, const char* in) { *s << String(in); } + }; + + template <> + struct StringStreamBase + { + template + static void convert(std::ostream* s, const T& in) { + *s << in; + } + }; + + template + struct StringStream : public StringStreamBase::value> + {}; + + template + void toStream(std::ostream* s, const T& value) { + StringStream::convert(s, value); + } + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + DOCTEST_INTERFACE void toStream(std::ostream* s, char* in); + DOCTEST_INTERFACE void toStream(std::ostream* s, const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + DOCTEST_INTERFACE void toStream(std::ostream* s, bool in); + DOCTEST_INTERFACE void toStream(std::ostream* s, float in); + DOCTEST_INTERFACE void toStream(std::ostream* s, double in); + DOCTEST_INTERFACE void toStream(std::ostream* s, double long in); + + DOCTEST_INTERFACE void toStream(std::ostream* s, char in); + DOCTEST_INTERFACE void toStream(std::ostream* s, char signed in); + DOCTEST_INTERFACE void toStream(std::ostream* s, char unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int short in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int short unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long unsigned in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long long in); + DOCTEST_INTERFACE void toStream(std::ostream* s, int long long unsigned in); + + // ContextScope base class used to allow implementing methods of ContextScope + // that don't depend on the template parameter in doctest.cpp. + class DOCTEST_INTERFACE ContextScopeBase : public IContextScope { + protected: + ContextScopeBase(); + + void destroy(); + }; + + template class ContextScope : public ContextScopeBase + { + const L &lambda_; + + public: + explicit ContextScope(const L &lambda) : lambda_(lambda) {} + + ContextScope(ContextScope &&other) : lambda_(other.lambda_) {} + + void stringify(std::ostream* s) const override { lambda_(s); } + + ~ContextScope() override { destroy(); } + }; + + struct DOCTEST_INTERFACE MessageBuilder : public MessageData + { + std::ostream* m_stream; + + MessageBuilder(const char* file, int line, assertType::Enum severity); + MessageBuilder() = delete; + ~MessageBuilder(); + + template + MessageBuilder& operator<<(const T& in) { + toStream(m_stream, in); + return *this; + } + + bool log(); + void react(); + }; + + template + ContextScope MakeContextScope(const L &lambda) { + return ContextScope(lambda); + } +} // namespace detail + +#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ + struct name \ + { \ + type data; \ + name(type in = def) \ + : data(in) {} \ + void fill(detail::TestCase& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + void fill(detail::TestSuite& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + } + +DOCTEST_DEFINE_DECORATOR(test_suite, const char*, ""); +DOCTEST_DEFINE_DECORATOR(description, const char*, ""); +DOCTEST_DEFINE_DECORATOR(skip, bool, true); +DOCTEST_DEFINE_DECORATOR(timeout, double, 0); +DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); + +template +int registerExceptionTranslator(String (*translateFunction)(T)) { + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") + static detail::ExceptionTranslator exceptionTranslator(translateFunction); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + detail::registerExceptionTranslatorImpl(&exceptionTranslator); + return 0; +} + +} // namespace doctest + +// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro +// introduces an anonymous namespace in which getCurrentTestSuite gets overridden +namespace doctest_detail_test_suite_ns { +DOCTEST_INTERFACE doctest::detail::TestSuite& getCurrentTestSuite(); +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +#else // DOCTEST_CONFIG_DISABLE +template +int registerExceptionTranslator(String (*)(T)) { + return 0; +} +#endif // DOCTEST_CONFIG_DISABLE + +namespace detail { + typedef void (*assert_handler)(const AssertData&); + struct ContextState; +} // namespace detail + +class DOCTEST_INTERFACE Context +{ + detail::ContextState* p; + + void parseArgs(int argc, const char* const* argv, bool withDefaults = false); + +public: + explicit Context(int argc = 0, const char* const* argv = nullptr); + + ~Context(); + + void applyCommandLine(int argc, const char* const* argv); + + void addFilter(const char* filter, const char* value); + void clearFilters(); + void setOption(const char* option, int value); + void setOption(const char* option, const char* value); + + bool shouldExit(); + + void setAsDefaultForAssertsOutOfTestCases(); + + void setAssertHandler(detail::assert_handler ah); + + int run(); +}; + +namespace TestCaseFailureReason { + enum Enum + { + None = 0, + AssertFailure = 1, // an assertion has failed in the test case + Exception = 2, // test case threw an exception + Crash = 4, // a crash... + TooManyFailedAsserts = 8, // the abort-after option + Timeout = 16, // see the timeout decorator + ShouldHaveFailedButDidnt = 32, // see the should_fail decorator + ShouldHaveFailedAndDid = 64, // see the should_fail decorator + DidntFailExactlyNumTimes = 128, // see the expected_failures decorator + FailedExactlyNumTimes = 256, // see the expected_failures decorator + CouldHaveFailedAndDid = 512 // see the may_fail decorator + }; +} // namespace TestCaseFailureReason + +struct DOCTEST_INTERFACE CurrentTestCaseStats +{ + int numAssertsCurrentTest; + int numAssertsFailedCurrentTest; + double seconds; + int failure_flags; // use TestCaseFailureReason::Enum +}; + +struct DOCTEST_INTERFACE TestCaseException +{ + String error_string; + bool is_crash; +}; + +struct DOCTEST_INTERFACE TestRunStats +{ + unsigned numTestCases; + unsigned numTestCasesPassingFilters; + unsigned numTestSuitesPassingFilters; + unsigned numTestCasesFailed; + int numAsserts; + int numAssertsFailed; +}; + +struct QueryData +{ + const TestRunStats* run_stats = nullptr; + const TestCaseData** data = nullptr; + unsigned num_data = 0; +}; + +struct DOCTEST_INTERFACE IReporter +{ + // The constructor has to accept "const ContextOptions&" as a single argument + // which has most of the options for the run + a pointer to the stdout stream + // Reporter(const ContextOptions& in) + + // called when a query should be reported (listing test cases, printing the version, etc.) + virtual void report_query(const QueryData&) = 0; + + // called when the whole test run starts + virtual void test_run_start() = 0; + // called when the whole test run ends (caching a pointer to the input doesn't make sense here) + virtual void test_run_end(const TestRunStats&) = 0; + + // called when a test case is started (safe to cache a pointer to the input) + virtual void test_case_start(const TestCaseData&) = 0; + // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) + virtual void test_case_reenter(const TestCaseData&) = 0; + // called when a test case has ended + virtual void test_case_end(const CurrentTestCaseStats&) = 0; + + // called when an exception is thrown from the test case (or it crashes) + virtual void test_case_exception(const TestCaseException&) = 0; + + // called whenever a subcase is entered (don't cache pointers to the input) + virtual void subcase_start(const SubcaseSignature&) = 0; + // called whenever a subcase is exited (don't cache pointers to the input) + virtual void subcase_end() = 0; + + // called for each assert (don't cache pointers to the input) + virtual void log_assert(const AssertData&) = 0; + // called for each message (don't cache pointers to the input) + virtual void log_message(const MessageData&) = 0; + + // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator + // or isn't in the execution range (between first and last) (safe to cache a pointer to the input) + virtual void test_case_skipped(const TestCaseData&) = 0; + + // doctest will not be managing the lifetimes of reporters given to it but this would still be nice to have + virtual ~IReporter(); + + // can obtain all currently active contexts and stringify them if one wishes to do so + static int get_num_active_contexts(); + static const IContextScope* const* get_active_contexts(); + + // can iterate through contexts which have been stringified automatically in their destructors when an exception has been thrown + static int get_num_stringified_contexts(); + static const String* get_stringified_contexts(); +}; + +namespace detail { + typedef IReporter* (*reporterCreatorFunc)(const ContextOptions&); + + DOCTEST_INTERFACE void registerReporterImpl(const char* name, int prio, reporterCreatorFunc c, bool isReporter); + + template + IReporter* reporterCreator(const ContextOptions& o) { + return new Reporter(o); + } +} // namespace detail + +template +int registerReporter(const char* name, int priority, bool isReporter) { + detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); + return 0; +} +} // namespace doctest + +// if registering is not disabled +#if !defined(DOCTEST_CONFIG_DISABLE) + +// common code in asserts - for convenience +#define DOCTEST_ASSERT_LOG_AND_REACT(b) \ + if(b.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + b.react() + +#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) x; +#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) \ + try { \ + x; \ + } catch(...) { _DOCTEST_RB.translateException(); } +#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(x) \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ + static_cast(x); \ + DOCTEST_GCC_SUPPRESS_WARNING_POP +#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(x) x; +#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +// registers the test by initializing a dummy var with a function +#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ + global_prefix DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \ + doctest::detail::regTest( \ + doctest::detail::TestCase( \ + f, __FILE__, __LINE__, \ + doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ + decorators); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ + namespace { \ + struct der : public base \ + { \ + void f(); \ + }; \ + static void func() { \ + der v; \ + v.f(); \ + } \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ + } \ + inline DOCTEST_NOINLINE void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ + static void f(); \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ + static void f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ + static doctest::detail::funcType proxy() { return f; } \ + DOCTEST_REGISTER_FUNCTION(inline const, proxy(), decorators) \ + static void f() + +// for registering tests +#define DOCTEST_TEST_CASE(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), decorators) + +// for registering tests in classes - requires C++17 for inline variables! +#if __cplusplus >= 201703L || (DOCTEST_MSVC >= DOCTEST_COMPILER(19, 12, 0) && _MSVC_LANG >= 201703L) +#define DOCTEST_TEST_CASE_CLASS(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_PROXY_), \ + decorators) +#else // DOCTEST_TEST_CASE_CLASS +#define DOCTEST_TEST_CASE_CLASS(...) \ + TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER +#endif // DOCTEST_TEST_CASE_CLASS + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(_DOCTEST_ANON_CLASS_), c, \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), decorators) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_IMPL(...) \ + template <> \ + inline const char* type_to_string<__VA_ARGS__>() { \ + return "<" #__VA_ARGS__ ">"; \ + } +#define DOCTEST_TYPE_TO_STRING(...) \ + namespace doctest { namespace detail { \ + DOCTEST_TYPE_TO_STRING_IMPL(__VA_ARGS__) \ + } \ + } \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ + template \ + static void func(); \ + namespace { \ + template \ + struct iter; \ + template \ + struct iter> \ + { \ + iter(const char* file, unsigned line, int index) { \ + doctest::detail::regTest(doctest::detail::TestCase(func, file, line, \ + doctest_detail_test_suite_ns::getCurrentTestSuite(), \ + doctest::detail::type_to_string(), \ + int(line) * 1000 + index) \ + * dec); \ + iter>(file, line, index + 1); \ + } \ + }; \ + template <> \ + struct iter> \ + { \ + iter(const char*, unsigned, int) {} \ + }; \ + } \ + template \ + static void func() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_)) + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_CAT(anon, DUMMY)) = \ + doctest::detail::instantiationHelper(DOCTEST_CAT(id, ITERATOR)<__VA_ARGS__>(__FILE__, __LINE__, 0));\ + DOCTEST_GLOBAL_NO_WARNINGS_END() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_), __VA_ARGS__) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ + template \ + static void anon() + +#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_), __VA_ARGS__) + +// for subcases +#define DOCTEST_SUBCASE(name) \ + if(const doctest::detail::Subcase & DOCTEST_ANONYMOUS(_DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ + doctest::detail::Subcase(name, __FILE__, __LINE__)) + +// for grouping tests in test suites by using code blocks +#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ + namespace ns_name { namespace doctest_detail_test_suite_ns { \ + static DOCTEST_NOINLINE doctest::detail::TestSuite& getCurrentTestSuite() { \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ + static doctest::detail::TestSuite data; \ + static bool inited = false; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + if(!inited) { \ + data* decorators; \ + inited = true; \ + } \ + return data; \ + } \ + } \ + } \ + namespace ns_name + +#define DOCTEST_TEST_SUITE(decorators) \ + DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(_DOCTEST_ANON_SUITE_)) + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_VAR_)) = \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * ""); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for registering exception translators +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ + inline doctest::String translatorName(signature); \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_TRANSLATOR_)) = \ + doctest::registerExceptionTranslator(translatorName); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() \ + doctest::String translatorName(signature) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(_DOCTEST_ANON_TRANSLATOR_), \ + signature) + +// for registering reporters +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_REPORTER_)) = \ + doctest::registerReporter(name, priority, true); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for registering listeners +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(_DOCTEST_ANON_REPORTER_)) = \ + doctest::registerReporter(name, priority, false); \ + DOCTEST_GLOBAL_NO_WARNINGS_END() typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for logging +#define DOCTEST_INFO(expression) \ + DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_), DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_), \ + DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_), expression) + +#define DOCTEST_INFO_IMPL(lambda_name, mb_name, s_name, expression) \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4626) \ + auto lambda_name = [&](std::ostream* s_name) { \ + doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ + mb_name.m_stream = s_name; \ + mb_name << expression; \ + }; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + auto DOCTEST_ANONYMOUS(_DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope(lambda_name) + +#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := " << x) + +#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, x) \ + do { \ + doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ + mb << x; \ + DOCTEST_ASSERT_LOG_AND_REACT(mb); \ + } while((void)0, 0) + +// clang-format off +#define DOCTEST_ADD_MESSAGE_AT(file, line, x) DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(_DOCTEST_MESSAGE_), x) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, x) DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(_DOCTEST_MESSAGE_), x) +#define DOCTEST_ADD_FAIL_AT(file, line, x) DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(_DOCTEST_MESSAGE_), x) +// clang-format on + +#define DOCTEST_MESSAGE(x) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, x) +#define DOCTEST_FAIL_CHECK(x) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, x) +#define DOCTEST_FAIL(x) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, x) + +#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(_DOCTEST_RB.setResult( \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB) \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + do { \ + DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ + } while((void)0, 0) + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +// necessary for _MESSAGE +#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::decomp_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__) DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) +#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) +#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) +#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) + +// clang-format off +#define DOCTEST_WARN_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); } while((void)0, 0) +#define DOCTEST_CHECK_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); } while((void)0, 0) +#define DOCTEST_REQUIRE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); } while((void)0, 0) +#define DOCTEST_WARN_FALSE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); } while((void)0, 0) +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); } while((void)0, 0) +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, msg) do { DOCTEST_INFO(msg); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); } while((void)0, 0) +// clang-format on + +#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ + do { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr, #__VA_ARGS__, message); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(const doctest::detail::remove_const< \ + doctest::detail::remove_reference<__VA_ARGS__>::type>::type&) { \ + _DOCTEST_RB.translateException(); \ + _DOCTEST_RB.m_threw_as = true; \ + } catch(...) { _DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } \ + } while((void)0, 0) + +#define DOCTEST_ASSERT_THROWS_WITH(expr, assert_type, ...) \ + do { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr, "", __VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(...) { _DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } \ + } while((void)0, 0) + +#define DOCTEST_ASSERT_NOTHROW(expr, assert_type) \ + do { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(...) { _DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } while((void)0, 0) + +// clang-format off +#define DOCTEST_WARN_THROWS(expr) DOCTEST_ASSERT_THROWS_WITH(expr, DT_WARN_THROWS, "") +#define DOCTEST_CHECK_THROWS(expr) DOCTEST_ASSERT_THROWS_WITH(expr, DT_CHECK_THROWS, "") +#define DOCTEST_REQUIRE_THROWS(expr) DOCTEST_ASSERT_THROWS_WITH(expr, DT_REQUIRE_THROWS, "") + +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, DT_WARN_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) + +#define DOCTEST_WARN_NOTHROW(expr) DOCTEST_ASSERT_NOTHROW(expr, DT_WARN_NOTHROW) +#define DOCTEST_CHECK_NOTHROW(expr) DOCTEST_ASSERT_NOTHROW(expr, DT_CHECK_NOTHROW) +#define DOCTEST_REQUIRE_NOTHROW(expr) DOCTEST_ASSERT_NOTHROW(expr, DT_REQUIRE_NOTHROW) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS(expr); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS(expr); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS(expr); } while((void)0, 0) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS_AS(expr, ex); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS_AS(expr, ex); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS_AS(expr, ex); } while((void)0, 0) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS_WITH(expr, with); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS_WITH(expr, with); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS_WITH(expr, with); } while((void)0, 0) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); } while((void)0, 0) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); } while((void)0, 0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); } while((void)0, 0) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_WARN_NOTHROW(expr); } while((void)0, 0) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_CHECK_NOTHROW(expr); } while((void)0, 0) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, msg) do { DOCTEST_INFO(msg); DOCTEST_REQUIRE_NOTHROW(expr); } while((void)0, 0) +// clang-format on + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ + do { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY( \ + _DOCTEST_RB.binary_assert( \ + __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } while((void)0, 0) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + do { \ + doctest::detail::ResultBuilder _DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(_DOCTEST_RB.unary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_AND_REACT(_DOCTEST_RB); \ + } while((void)0, 0) + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ + doctest::detail::binary_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \ + #__VA_ARGS__, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) +#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) +#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) +#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) +#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) +#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) +#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) +#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) +#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) +#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) +#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) +#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) +#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) +#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) +#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) +#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) +#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) + +#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS + +#undef DOCTEST_WARN_THROWS +#undef DOCTEST_CHECK_THROWS +#undef DOCTEST_REQUIRE_THROWS +#undef DOCTEST_WARN_THROWS_AS +#undef DOCTEST_CHECK_THROWS_AS +#undef DOCTEST_REQUIRE_THROWS_AS +#undef DOCTEST_WARN_THROWS_WITH +#undef DOCTEST_CHECK_THROWS_WITH +#undef DOCTEST_REQUIRE_THROWS_WITH +#undef DOCTEST_WARN_THROWS_WITH_AS +#undef DOCTEST_CHECK_THROWS_WITH_AS +#undef DOCTEST_REQUIRE_THROWS_WITH_AS +#undef DOCTEST_WARN_NOTHROW +#undef DOCTEST_CHECK_NOTHROW +#undef DOCTEST_REQUIRE_NOTHROW + +#undef DOCTEST_WARN_THROWS_MESSAGE +#undef DOCTEST_CHECK_THROWS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_MESSAGE +#undef DOCTEST_WARN_THROWS_AS_MESSAGE +#undef DOCTEST_CHECK_THROWS_AS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_AS_MESSAGE +#undef DOCTEST_WARN_THROWS_WITH_MESSAGE +#undef DOCTEST_CHECK_THROWS_WITH_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_WITH_MESSAGE +#undef DOCTEST_WARN_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE +#undef DOCTEST_WARN_NOTHROW_MESSAGE +#undef DOCTEST_CHECK_NOTHROW_MESSAGE +#undef DOCTEST_REQUIRE_NOTHROW_MESSAGE + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#define DOCTEST_WARN_THROWS(expr) ((void)0) +#define DOCTEST_CHECK_THROWS(expr) ((void)0) +#define DOCTEST_REQUIRE_THROWS(expr) ((void)0) +#define DOCTEST_WARN_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_WARN_NOTHROW(expr) ((void)0) +#define DOCTEST_CHECK_NOTHROW(expr) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW(expr) ((void)0) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, msg) ((void)0) + +#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#undef DOCTEST_REQUIRE +#undef DOCTEST_REQUIRE_FALSE +#undef DOCTEST_REQUIRE_MESSAGE +#undef DOCTEST_REQUIRE_FALSE_MESSAGE +#undef DOCTEST_REQUIRE_EQ +#undef DOCTEST_REQUIRE_NE +#undef DOCTEST_REQUIRE_GT +#undef DOCTEST_REQUIRE_LT +#undef DOCTEST_REQUIRE_GE +#undef DOCTEST_REQUIRE_LE +#undef DOCTEST_REQUIRE_UNARY +#undef DOCTEST_REQUIRE_UNARY_FALSE + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// ================================================================================================= +// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == +// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == +// ================================================================================================= +#else // DOCTEST_CONFIG_DISABLE + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ + namespace { \ + template \ + struct der : public base \ + { void f(); }; \ + } \ + template \ + inline void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ + template \ + static inline void f() + +// for registering tests +#define DOCTEST_TEST_CASE(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), name) + +// for registering tests in classes +#define DOCTEST_TEST_CASE_CLASS(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), name) + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(_DOCTEST_ANON_CLASS_), x, \ + DOCTEST_ANONYMOUS(_DOCTEST_ANON_FUNC_), name) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING(...) typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) +#define DOCTEST_TYPE_TO_STRING_IMPL(...) + +// for typed tests +#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ + template \ + inline void DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ + template \ + inline void DOCTEST_ANONYMOUS(_DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for subcases +#define DOCTEST_SUBCASE(name) + +// for a testsuite block +#define DOCTEST_TEST_SUITE(name) namespace + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(name) typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END typedef int DOCTEST_ANONYMOUS(_DOCTEST_ANON_FOR_SEMICOLON_) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + template \ + static inline doctest::String DOCTEST_ANONYMOUS(_DOCTEST_ANON_TRANSLATOR_)(signature) + +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) + +#define DOCTEST_INFO(x) ((void)0) +#define DOCTEST_CAPTURE(x) ((void)0) +#define DOCTEST_ADD_MESSAGE_AT(file, line, x) ((void)0) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, x) ((void)0) +#define DOCTEST_ADD_FAIL_AT(file, line, x) ((void)0) +#define DOCTEST_MESSAGE(x) ((void)0) +#define DOCTEST_FAIL_CHECK(x) ((void)0) +#define DOCTEST_FAIL(x) ((void)0) + +#define DOCTEST_WARN(...) ((void)0) +#define DOCTEST_CHECK(...) ((void)0) +#define DOCTEST_REQUIRE(...) ((void)0) +#define DOCTEST_WARN_FALSE(...) ((void)0) +#define DOCTEST_CHECK_FALSE(...) ((void)0) +#define DOCTEST_REQUIRE_FALSE(...) ((void)0) + +#define DOCTEST_WARN_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_CHECK_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_REQUIRE_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_WARN_FALSE_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, msg) ((void)0) +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, msg) ((void)0) + +#define DOCTEST_WARN_THROWS(expr) ((void)0) +#define DOCTEST_CHECK_THROWS(expr) ((void)0) +#define DOCTEST_REQUIRE_THROWS(expr) ((void)0) +#define DOCTEST_WARN_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) ((void)0) +#define DOCTEST_WARN_NOTHROW(expr) ((void)0) +#define DOCTEST_CHECK_NOTHROW(expr) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW(expr) ((void)0) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, msg) ((void)0) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, msg) ((void)0) +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, msg) ((void)0) +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, msg) ((void)0) + +#define DOCTEST_WARN_EQ(...) ((void)0) +#define DOCTEST_CHECK_EQ(...) ((void)0) +#define DOCTEST_REQUIRE_EQ(...) ((void)0) +#define DOCTEST_WARN_NE(...) ((void)0) +#define DOCTEST_CHECK_NE(...) ((void)0) +#define DOCTEST_REQUIRE_NE(...) ((void)0) +#define DOCTEST_WARN_GT(...) ((void)0) +#define DOCTEST_CHECK_GT(...) ((void)0) +#define DOCTEST_REQUIRE_GT(...) ((void)0) +#define DOCTEST_WARN_LT(...) ((void)0) +#define DOCTEST_CHECK_LT(...) ((void)0) +#define DOCTEST_REQUIRE_LT(...) ((void)0) +#define DOCTEST_WARN_GE(...) ((void)0) +#define DOCTEST_CHECK_GE(...) ((void)0) +#define DOCTEST_REQUIRE_GE(...) ((void)0) +#define DOCTEST_WARN_LE(...) ((void)0) +#define DOCTEST_CHECK_LE(...) ((void)0) +#define DOCTEST_REQUIRE_LE(...) ((void)0) + +#define DOCTEST_WARN_UNARY(...) ((void)0) +#define DOCTEST_CHECK_UNARY(...) ((void)0) +#define DOCTEST_REQUIRE_UNARY(...) ((void)0) +#define DOCTEST_WARN_UNARY_FALSE(...) ((void)0) +#define DOCTEST_CHECK_UNARY_FALSE(...) ((void)0) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) ((void)0) + +#endif // DOCTEST_CONFIG_DISABLE + +// clang-format off +// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS +#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ +#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ +#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE +#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE +#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE +#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT +#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT +#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT +#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT +#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT +#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT +#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE +#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE +#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE +#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE +#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE +#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE + +#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY +#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY +#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE DOCTEST_TEST_CASE_TEMPLATE_INVOKE +// clang-format on + +// BDD style macros +// clang-format off +#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) +#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) +#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) +#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) + +#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) +#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) +#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE("And when: " name) +#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) +#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) +// clang-format on + +// == SHORT VERSIONS OF THE MACROS +#if !defined(DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES) + +#define TEST_CASE DOCTEST_TEST_CASE +#define TEST_CASE_CLASS DOCTEST_TEST_CASE_CLASS +#define TEST_CASE_FIXTURE DOCTEST_TEST_CASE_FIXTURE +#define TYPE_TO_STRING DOCTEST_TYPE_TO_STRING +#define TEST_CASE_TEMPLATE DOCTEST_TEST_CASE_TEMPLATE +#define TEST_CASE_TEMPLATE_DEFINE DOCTEST_TEST_CASE_TEMPLATE_DEFINE +#define TEST_CASE_TEMPLATE_INVOKE DOCTEST_TEST_CASE_TEMPLATE_INVOKE +#define TEST_CASE_TEMPLATE_APPLY DOCTEST_TEST_CASE_TEMPLATE_APPLY +#define SUBCASE DOCTEST_SUBCASE +#define TEST_SUITE DOCTEST_TEST_SUITE +#define TEST_SUITE_BEGIN DOCTEST_TEST_SUITE_BEGIN +#define TEST_SUITE_END DOCTEST_TEST_SUITE_END +#define REGISTER_EXCEPTION_TRANSLATOR DOCTEST_REGISTER_EXCEPTION_TRANSLATOR +#define REGISTER_REPORTER DOCTEST_REGISTER_REPORTER +#define REGISTER_LISTENER DOCTEST_REGISTER_LISTENER +#define INFO DOCTEST_INFO +#define CAPTURE DOCTEST_CAPTURE +#define ADD_MESSAGE_AT DOCTEST_ADD_MESSAGE_AT +#define ADD_FAIL_CHECK_AT DOCTEST_ADD_FAIL_CHECK_AT +#define ADD_FAIL_AT DOCTEST_ADD_FAIL_AT +#define MESSAGE DOCTEST_MESSAGE +#define FAIL_CHECK DOCTEST_FAIL_CHECK +#define FAIL DOCTEST_FAIL +#define TO_LVALUE DOCTEST_TO_LVALUE + +#define WARN DOCTEST_WARN +#define WARN_FALSE DOCTEST_WARN_FALSE +#define WARN_THROWS DOCTEST_WARN_THROWS +#define WARN_THROWS_AS DOCTEST_WARN_THROWS_AS +#define WARN_THROWS_WITH DOCTEST_WARN_THROWS_WITH +#define WARN_THROWS_WITH_AS DOCTEST_WARN_THROWS_WITH_AS +#define WARN_NOTHROW DOCTEST_WARN_NOTHROW +#define CHECK DOCTEST_CHECK +#define CHECK_FALSE DOCTEST_CHECK_FALSE +#define CHECK_THROWS DOCTEST_CHECK_THROWS +#define CHECK_THROWS_AS DOCTEST_CHECK_THROWS_AS +#define CHECK_THROWS_WITH DOCTEST_CHECK_THROWS_WITH +#define CHECK_THROWS_WITH_AS DOCTEST_CHECK_THROWS_WITH_AS +#define CHECK_NOTHROW DOCTEST_CHECK_NOTHROW +#define REQUIRE DOCTEST_REQUIRE +#define REQUIRE_FALSE DOCTEST_REQUIRE_FALSE +#define REQUIRE_THROWS DOCTEST_REQUIRE_THROWS +#define REQUIRE_THROWS_AS DOCTEST_REQUIRE_THROWS_AS +#define REQUIRE_THROWS_WITH DOCTEST_REQUIRE_THROWS_WITH +#define REQUIRE_THROWS_WITH_AS DOCTEST_REQUIRE_THROWS_WITH_AS +#define REQUIRE_NOTHROW DOCTEST_REQUIRE_NOTHROW + +#define WARN_MESSAGE DOCTEST_WARN_MESSAGE +#define WARN_FALSE_MESSAGE DOCTEST_WARN_FALSE_MESSAGE +#define WARN_THROWS_MESSAGE DOCTEST_WARN_THROWS_MESSAGE +#define WARN_THROWS_AS_MESSAGE DOCTEST_WARN_THROWS_AS_MESSAGE +#define WARN_THROWS_WITH_MESSAGE DOCTEST_WARN_THROWS_WITH_MESSAGE +#define WARN_THROWS_WITH_AS_MESSAGE DOCTEST_WARN_THROWS_WITH_AS_MESSAGE +#define WARN_NOTHROW_MESSAGE DOCTEST_WARN_NOTHROW_MESSAGE +#define CHECK_MESSAGE DOCTEST_CHECK_MESSAGE +#define CHECK_FALSE_MESSAGE DOCTEST_CHECK_FALSE_MESSAGE +#define CHECK_THROWS_MESSAGE DOCTEST_CHECK_THROWS_MESSAGE +#define CHECK_THROWS_AS_MESSAGE DOCTEST_CHECK_THROWS_AS_MESSAGE +#define CHECK_THROWS_WITH_MESSAGE DOCTEST_CHECK_THROWS_WITH_MESSAGE +#define CHECK_THROWS_WITH_AS_MESSAGE DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE +#define CHECK_NOTHROW_MESSAGE DOCTEST_CHECK_NOTHROW_MESSAGE +#define REQUIRE_MESSAGE DOCTEST_REQUIRE_MESSAGE +#define REQUIRE_FALSE_MESSAGE DOCTEST_REQUIRE_FALSE_MESSAGE +#define REQUIRE_THROWS_MESSAGE DOCTEST_REQUIRE_THROWS_MESSAGE +#define REQUIRE_THROWS_AS_MESSAGE DOCTEST_REQUIRE_THROWS_AS_MESSAGE +#define REQUIRE_THROWS_WITH_MESSAGE DOCTEST_REQUIRE_THROWS_WITH_MESSAGE +#define REQUIRE_THROWS_WITH_AS_MESSAGE DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE +#define REQUIRE_NOTHROW_MESSAGE DOCTEST_REQUIRE_NOTHROW_MESSAGE + +#define SCENARIO DOCTEST_SCENARIO +#define SCENARIO_CLASS DOCTEST_SCENARIO_CLASS +#define SCENARIO_TEMPLATE DOCTEST_SCENARIO_TEMPLATE +#define SCENARIO_TEMPLATE_DEFINE DOCTEST_SCENARIO_TEMPLATE_DEFINE +#define GIVEN DOCTEST_GIVEN +#define WHEN DOCTEST_WHEN +#define AND_WHEN DOCTEST_AND_WHEN +#define THEN DOCTEST_THEN +#define AND_THEN DOCTEST_AND_THEN + +#define WARN_EQ DOCTEST_WARN_EQ +#define CHECK_EQ DOCTEST_CHECK_EQ +#define REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define WARN_NE DOCTEST_WARN_NE +#define CHECK_NE DOCTEST_CHECK_NE +#define REQUIRE_NE DOCTEST_REQUIRE_NE +#define WARN_GT DOCTEST_WARN_GT +#define CHECK_GT DOCTEST_CHECK_GT +#define REQUIRE_GT DOCTEST_REQUIRE_GT +#define WARN_LT DOCTEST_WARN_LT +#define CHECK_LT DOCTEST_CHECK_LT +#define REQUIRE_LT DOCTEST_REQUIRE_LT +#define WARN_GE DOCTEST_WARN_GE +#define CHECK_GE DOCTEST_CHECK_GE +#define REQUIRE_GE DOCTEST_REQUIRE_GE +#define WARN_LE DOCTEST_WARN_LE +#define CHECK_LE DOCTEST_CHECK_LE +#define REQUIRE_LE DOCTEST_REQUIRE_LE +#define WARN_UNARY DOCTEST_WARN_UNARY +#define CHECK_UNARY DOCTEST_CHECK_UNARY +#define REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +// KEPT FOR BACKWARDS COMPATIBILITY +#define FAST_WARN_EQ DOCTEST_FAST_WARN_EQ +#define FAST_CHECK_EQ DOCTEST_FAST_CHECK_EQ +#define FAST_REQUIRE_EQ DOCTEST_FAST_REQUIRE_EQ +#define FAST_WARN_NE DOCTEST_FAST_WARN_NE +#define FAST_CHECK_NE DOCTEST_FAST_CHECK_NE +#define FAST_REQUIRE_NE DOCTEST_FAST_REQUIRE_NE +#define FAST_WARN_GT DOCTEST_FAST_WARN_GT +#define FAST_CHECK_GT DOCTEST_FAST_CHECK_GT +#define FAST_REQUIRE_GT DOCTEST_FAST_REQUIRE_GT +#define FAST_WARN_LT DOCTEST_FAST_WARN_LT +#define FAST_CHECK_LT DOCTEST_FAST_CHECK_LT +#define FAST_REQUIRE_LT DOCTEST_FAST_REQUIRE_LT +#define FAST_WARN_GE DOCTEST_FAST_WARN_GE +#define FAST_CHECK_GE DOCTEST_FAST_CHECK_GE +#define FAST_REQUIRE_GE DOCTEST_FAST_REQUIRE_GE +#define FAST_WARN_LE DOCTEST_FAST_WARN_LE +#define FAST_CHECK_LE DOCTEST_FAST_CHECK_LE +#define FAST_REQUIRE_LE DOCTEST_FAST_REQUIRE_LE + +#define FAST_WARN_UNARY DOCTEST_FAST_WARN_UNARY +#define FAST_CHECK_UNARY DOCTEST_FAST_CHECK_UNARY +#define FAST_REQUIRE_UNARY DOCTEST_FAST_REQUIRE_UNARY +#define FAST_WARN_UNARY_FALSE DOCTEST_FAST_WARN_UNARY_FALSE +#define FAST_CHECK_UNARY_FALSE DOCTEST_FAST_CHECK_UNARY_FALSE +#define FAST_REQUIRE_UNARY_FALSE DOCTEST_FAST_REQUIRE_UNARY_FALSE + +#define TEST_CASE_TEMPLATE_INSTANTIATE DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE + +#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#if !defined(DOCTEST_CONFIG_DISABLE) + +// this is here to clear the 'current test suite' for the current translation unit - at the top +DOCTEST_TEST_SUITE_END(); + +// add stringification for primitive/fundamental types +namespace doctest { namespace detail { + DOCTEST_TYPE_TO_STRING_IMPL(bool) + DOCTEST_TYPE_TO_STRING_IMPL(float) + DOCTEST_TYPE_TO_STRING_IMPL(double) + DOCTEST_TYPE_TO_STRING_IMPL(long double) + DOCTEST_TYPE_TO_STRING_IMPL(char) + DOCTEST_TYPE_TO_STRING_IMPL(signed char) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned char) +#if !DOCTEST_MSVC || defined(_NATIVE_WCHAR_T_DEFINED) + DOCTEST_TYPE_TO_STRING_IMPL(wchar_t) +#endif // not MSVC or wchar_t support enabled + DOCTEST_TYPE_TO_STRING_IMPL(short int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned short int) + DOCTEST_TYPE_TO_STRING_IMPL(int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned int) + DOCTEST_TYPE_TO_STRING_IMPL(long int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned long int) + DOCTEST_TYPE_TO_STRING_IMPL(long long int) + DOCTEST_TYPE_TO_STRING_IMPL(unsigned long long int) +}} // namespace doctest::detail + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_LIBRARY_INCLUDED diff --git a/Firmware/fibre/cpp/endpoints_template.j2 b/Firmware/fibre/cpp/endpoints_template.j2 new file mode 100644 index 00000000..db9b8dbd --- /dev/null +++ b/Firmware/fibre/cpp/endpoints_template.j2 @@ -0,0 +1,90 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains the toplevel handler for Fibre v0.1 endpoint operations. + * + * This endpoint-oriented approach will be deprecated in Fibre v0.2 in favor of + * a function-oriented approach and a more powerful object model. + * + */ +#ifndef __FIBRE_INTERFACES_HPP +#define __FIBRE_INTERFACES_HPP + +#include + +// Note: with -Og the functions with large switch statements reserves a huge amount +// of stack space because they reserves separate space for the stack frame of each +// of the inlined functions. +// The minimum known set of flags to prevent this is `-O1 -fipa-sra`. +// `-O2`, `-O3` and `-Os` are supersets of this. + +#pragma GCC push_options +#pragma GCC optimize ("s") + +namespace fibre { + +const unsigned char embedded_json[] = [[embedded_endpoint_definitions | to_c_string]]; +const size_t embedded_json_length = sizeof(embedded_json) - 1; +const uint16_t json_crc_ = calc_crc16(PROTOCOL_VERSION, embedded_json, embedded_json_length); +const uint32_t json_version_id_ = (json_crc_ << 16) | calc_crc16(json_crc_, embedded_json, embedded_json_length); + +static void get_property(Introspectable& result, size_t idx) { + switch (idx) { +[%- for endpoint in endpoints %] +[%- if endpoint.function.name == 'exchange' and endpoint.in_bindings | list == ['obj'] %] + case [[endpoint.id]]: { [[(endpoint.in_bindings['obj'] + '$') | replace(')$', ', &result.storage_)')]]; result.type_info_ = &FibrePropertyTypeInfo<[[endpoint.function.in['obj'].type.c_name]]>::singleton; } break; +[%- endif %] +[%- endfor %] + default: break; + } +} + + +bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer) { + //Introspectable property = get_property(idx); + //if property.is_valid() + + switch (idx) { +[%- for endpoint in endpoints %] +[%- if (endpoint.function.name == 'exchange' or endpoint.function.name == 'read') and endpoint.in_bindings | list == ['obj'] %] + case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_name]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_name]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; +[%- else %] + case [[endpoint.id]]: { return [[endpoint.function.fullname | to_snake_case]]([% for k, arg in endpoint.function.in.items() %][% if k in endpoint.in_bindings %]static_cast<[[arg.type.c_name]]>([[endpoint.in_bindings[k]]])[% else %]std::nullopt[% endif %], [% endfor %][% for k, arg in endpoint.function.out.items() %][% if k in endpoint.out_bindings %]static_cast<[[arg.type.c_name]]*>([[endpoint.out_bindings[k]]])[% else %]nullptr[% endif %], [% endfor %]input_buffer, output_buffer); } break; +[%- endif %] +[%- endfor %] + default: return false; + } +} + +bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref) { + if (endpoint_ref.json_crc != json_crc_) { + return false; + } + + switch (endpoint_ref.endpoint_id) { +[%- for endpoint in endpoints %] + case [[endpoint.id]]: return true; +[%- endfor %] + default: return false; + } +} + +bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value) { + if (endpoint_ref.json_crc != json_crc_) { + return false; + } + + Introspectable property{}; + get_property(property, endpoint_ref.endpoint_id); + const FloatSettableTypeInfo* type_info = dynamic_cast(property.get_type_info()); + return type_info && type_info->set_float(property, value); +} + +} + +#pragma GCC pop_options + +#endif // __FIBRE_INTERFACES_HPP \ No newline at end of file diff --git a/Firmware/fibre/cpp/function_stubs_template.j2 b/Firmware/fibre/cpp/function_stubs_template.j2 new file mode 100644 index 00000000..fb44fdaa --- /dev/null +++ b/Firmware/fibre/cpp/function_stubs_template.j2 @@ -0,0 +1,40 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains serializing/deserializing stubs for the functions defined + * in your interface file. + * + */ + +#include + +[% for intf in interfaces.values() %] +[% for func in intf.functions.values() %] +static inline bool [[func.fullname | to_snake_case]]([% for arg in func.in.values() %]std::optional<[[arg.type.c_name]]> in_[[arg.name]], [% endfor %][% for arg in func.out.values() %][[arg.type.c_name]]* out_[[arg.name]], [% endfor %]fibre::cbufptr_t* input_buffer, fibre::bufptr_t* output_buffer) { +[%- if func.in %] + bool success = [% for arg in func.in.values() %](in_[[arg.name]].has_value() || (in_[[arg.name]] = fibre::Codec<[[arg.type.c_name]]>::decode(input_buffer)).has_value()[% if arg.optional %] || true[% endif %])[% if not loop.last %] + && [% endif %][% endfor %]; +[%- else %] + bool success = true; +[%- endif %] + if (!success) { + return false; + } +[%- if func.implementation %] + [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_name]][[', ' if not loop.last]][% endfor %]> ret = [% endif %][[func.implementation]]([% for arg in func.in.values() %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); +[%- else %] + [% if func.out %]std::tuple<[% for arg in func.out.values() %][[arg.type.c_name]][[', ' if not loop.last]][% endfor %]> ret = [% endif %]in_[[(func.in.values() | first).name]].value()->[[func.name]]([% for arg in func.in.values() | skip_first %]in_[[arg.name]][% if not arg.optional %].value()[% endif %][[', ' if not loop.last]][% endfor %]); +[%- endif %] +[%- if func.out %] + return [% for arg in func.out.values() %]((out_[[arg.name]] && ((*out_[[arg.name]] = std::get<[[loop.index0]]>(ret)), true)) || fibre::Codec<[[arg.type.c_name]]>::encode(std::get<[[loop.index0]]>(ret), output_buffer))[% if not loop.last %] + && [% endif %][% endfor %]; +[%- else %] + return true; +[%- endif %] +} +[% endfor %] +[% endfor %] + diff --git a/Firmware/fibre/cpp/include/fibre/bufptr.hpp b/Firmware/fibre/cpp/include/fibre/bufptr.hpp new file mode 100644 index 00000000..2ce3fefb --- /dev/null +++ b/Firmware/fibre/cpp/include/fibre/bufptr.hpp @@ -0,0 +1,93 @@ +#ifndef __FIBRE_BUFPTR_HPP +#define __FIBRE_BUFPTR_HPP + +namespace fibre { + +static inline bool soft_assert(bool expr) { return expr; } // TODO: implement + +/** + * @brief Holds a reference to a buffer and a length. + * Since this class implements begin() and end(), you can use it with many + * standard algorithms that operate on iterable objects. + */ +template +struct generic_bufptr_t { + using iterator = T*; + using const_iterator = const T*; + + generic_bufptr_t(T* begin, size_t length) : begin_(begin), end_(begin + length) {} + + generic_bufptr_t(T* begin, T* end) : begin_(begin), end_(end) {} + + generic_bufptr_t() : begin_(nullptr), end_(nullptr) {} + + template + generic_bufptr_t(T (&begin)[I]) : generic_bufptr_t(begin, I) {} + + generic_bufptr_t(const std::vector>& vector) + : generic_bufptr_t(vector.data(), vector.size()) {} + + generic_bufptr_t(const generic_bufptr_t>& other) + : generic_bufptr_t(other.begin_, other.end_) {} + + generic_bufptr_t& operator+=(size_t num) { + if (!soft_assert(num <= size())) { + num = size(); + } + begin_ += num; + return *this; + } + + generic_bufptr_t operator++(int) { + generic_bufptr_t result = *this; + *this += 1; + return result; + } + + T& operator*() { + return *begin_; + } + + generic_bufptr_t take(size_t num) const { + if (!soft_assert(num <= size())) { + num = size(); + } + generic_bufptr_t result = {begin_, num}; + return result; + } + + generic_bufptr_t skip(size_t num, size_t* processed_bytes = nullptr) const { + if (!soft_assert(num <= size())) { + num = size(); + } + if (processed_bytes) + (*processed_bytes) += num; + return {begin_ + num, end_}; + } + + size_t size() const { + return end_ - begin_; + } + + bool empty() const { + return size() == 0; + } + + T*& begin() { return begin_; } + T*& end() { return end_; } + T* const & begin() const { return begin_; } + T* const & end() const { return end_; } + T& front() const { return *begin(); } + T& back() const { return *(end() - 1); } + T& operator[](size_t idx) { return *(begin() + idx); } + + T* begin_; + T* end_; +}; + +using cbufptr_t = generic_bufptr_t; +using bufptr_t = generic_bufptr_t; + +} + +#endif // __FIBRE_BUFPTR_HPP diff --git a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp index 4b97f367..b09aa400 100644 --- a/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp +++ b/Firmware/fibre/cpp/include/fibre/cpp_utils.hpp @@ -1,6 +1,3 @@ -#ifndef __CPP_UTILS_HPP -#define __CPP_UTILS_HPP - /* ## Advanced C++ Topics @@ -78,8 +75,18 @@ public: */ -// Backport definitions from C++14 -#if __cplusplus <= 201103L +#ifndef __CPP_UTILS_HPP +#define __CPP_UTILS_HPP + +#include +#include +#include +#include +#include + +/* Backport features from C++14 and C++17 ------------------------------------*/ + +#if __cplusplus < 201402L namespace std { template< class T > using underlying_type_t = typename underlying_type::type; @@ -87,9 +94,377 @@ namespace std { // source: http://en.cppreference.com/w/cpp/types/enable_if template< bool B, class T = void > using enable_if_t = typename enable_if::type; + + // source: https://en.cppreference.com/w/cpp/types/conditional + template< bool B, class T, class F > + using conditional_t = typename conditional::type; + + // source: http://en.cppreference.com/w/cpp/utility/tuple/tuple_element + template + using tuple_element_t = typename tuple_element::type; + + // source: https://en.cppreference.com/w/cpp/types/remove_cv + template< class T > + using remove_cv_t = typename remove_cv::type; + template< class T > + using remove_const_t = typename remove_const::type; + template< class T > + using remove_volatile_t = typename remove_volatile::type; + template< class T > + using remove_reference_t = typename remove_reference::type; + + template< class T > + using decay_t = typename decay::type; + + // integer_sequence implementation adapted from + // https://stackoverflow.com/questions/17424477/implementation-c14-make-integer-sequence + + /// Class template integer_sequence + template + struct integer_sequence { + using type = integer_sequence; + typedef _Tp value_type; + static constexpr size_t size() noexcept { return sizeof...(_Idx); } + }; + + template + struct _merge_and_renumber; + + template + struct _merge_and_renumber, integer_sequence<_Tp, I2...>> + : integer_sequence<_Tp, I1..., (sizeof...(I1)+I2)...> + { }; + + template + struct make_integer_sequence + : _merge_and_renumber::type, + typename make_integer_sequence<_Tp, N - N/2>::type> + { }; + + template struct make_integer_sequence<_Tp, 0> : integer_sequence<_Tp> { }; + template struct make_integer_sequence<_Tp, 1> : integer_sequence<_Tp, 0> { }; + + /// Alias template index_sequence + template + using index_sequence = integer_sequence; + + /// Alias template make_index_sequence + template + using make_index_sequence = typename make_integer_sequence::type; } #endif +namespace fibre { + // Creates the index sequence { IFrom, IFrom + 1, IFrom + 2, ..., ITo - 1 } + template + struct make_integer_sequence_from_to_impl { + using type = typename make_integer_sequence_from_to_impl<_Tp, IFrom, ITo - 1, ITo - 1, I...>::type; + }; + + template + struct make_integer_sequence_from_to_impl<_Tp, IFrom, IFrom, I...> { + using type = std::index_sequence; + }; + + template + using make_integer_sequence_from_to = typename make_integer_sequence_from_to_impl<_Tp, IFrom, ITo>::type; +} + +#if __cplusplus < 201703L +namespace std { +//template>{}, int> = 0> +//using enable_ + +template struct invoke_result_impl; + +template +struct invoke_result_impl>{}>, + Fn, Args...> { + typedef decltype(std::mem_fn(std::declval())(std::declval()...)) type; +}; + +template +struct invoke_result_impl>{}>, + Fn, Args...> { + typedef decltype(std::declval()(std::declval()...)) type; +}; + +template +using invoke_result = invoke_result_impl; + +template +using invoke_result_t = typename invoke_result::type; + +template>{}, int> = 0 > +constexpr invoke_result_t invoke(Fn&& f, Args&&... args) + noexcept(noexcept(std::mem_fn(f)(std::forward(args)...))) +{ + return std::mem_fn(f)(std::forward(args)...); +} + +template>{}, int> = 0> +constexpr invoke_result_t invoke(Fn&& f, Args&&... args) + noexcept(noexcept(std::forward(f)(std::forward(args)...))) +{ + return std::forward(f)(std::forward(args)...); +} +} + +namespace std { +namespace detail { +template +struct apply_result_impl; + +// TODO: apply_result is not part of C++17, therefore we should move this out of +// the #if block +template +struct apply_result_impl> { + //typedef std::invoke_result_t...> type; + typedef std::invoke_result_t(std::declval()))...> type; +}; + +template +using apply_result = apply_result_impl>::value>>; + +template +using apply_result_t = typename apply_result::type; + +template +constexpr apply_result_t apply_impl( F&& f, Tuple&& t, std::index_sequence ) +{ + return std::invoke(std::forward(f), std::get(std::forward(t))...); +} +} // namespace detail + +template +constexpr detail::apply_result_t apply(F&& f, Tuple&& t) +{ + return detail::apply_impl(std::forward(f), std::forward(t), + std::make_index_sequence>::value>{}); +} +} + + +namespace std { + +template +struct identity { using type = T; }; + +template +struct overload_resolver; + +template<> +struct overload_resolver<> { void operator()() const; }; + +template +struct overload_resolver : overload_resolver { + using overload_resolver::operator(); + identity operator()(T) const; +}; + +template +struct index_of : integral_constant::value + 1)> {}; + +template +struct index_of : integral_constant {}; + +/** + * @brief Heavily simplified version of the C++17 std::variant. + * Whatever compiles should work as one would expect from the C++17 variant. + */ +template +class variant; + +// Empty variant is ill-formed. Only used for clean recursion here. +template<> +class variant<> { +public: + using storage_t = char[0]; + storage_t content_; + + static void selective_destructor(char* storage, size_t index) { + throw; + } + + static void selective_copy_constuctor(char* target, const char* source, size_t index) { + throw; + } + + static bool selective_eq(const char* lhs, const char* rhs, size_t index) { + throw; + } + + static bool selective_neq(const char* lhs, const char* rhs, size_t index) { + throw; + } + + template + static void selective_invoke_const(const char* content, size_t index, TFunc functor, TArgs&&... args) { + throw; + } + + template + static void selective_invoke(const char* content, size_t index, TFunc functor, TArgs&&... args) { + throw; + } +}; + +template +class variant { +public: + using storage_t = char[sizeof(T) > sizeof(typename variant::storage_t) ? sizeof(T) : sizeof(typename variant::storage_t)]; + + static void selective_copy_constuctor(char* target, const char* source, size_t index) { + if (index == 0) { + new ((T*)target) T{*(T*)source}; // in-place construction using first type's copy constructor + } else { + variant::selective_copy_constuctor(target, source, index - 1); + } + } + + static void selective_destructor(char* storage, size_t index) { + if (index == 0) { + ((T*)storage)->~T(); + } else { + variant::selective_destructor(storage, index - 1); + } + } + + static bool selective_eq(const char* lhs, const char* rhs, size_t index) { + if (index == 0) { + return ((*(T*)lhs) == (*(T*)rhs)); + } else { + return variant::selective_eq(lhs, rhs, index - 1); + } + } + + static bool selective_neq(const char* lhs, const char* rhs, size_t index) { + if (index == 0) { + return ((*(T*)lhs) != (*(T*)rhs)); + } else { + return variant::selective_neq(lhs, rhs, index - 1); + } + } + + template + static void selective_invoke_const(const char* content, size_t index, TFunc functor, TArgs&&... args) { + if (index == 0) { + functor(*(T*)content, std::forward(args)...); + } else { + variant::selective_invoke_const(content, index - 1, functor, std::forward(args)...); + } + } + + template + static void selective_invoke(char* content, size_t index, TFunc functor, TArgs&&... args) { + if (index == 0) { + functor(*(T*)content, std::forward(args)...); + } else { + variant::selective_invoke(content, index - 1, functor, std::forward(args)...); + } + } + + variant() : index_(0) { + new ((T*)content_) T{}; // in-place construction using first type's default constructor + } + + variant(const variant & other) : index_(other.index_) { + selective_copy_constuctor(content_, other.content_, index_); + } + + variant(variant&& other) : index_(other.index_) { + // TODO: implement + selective_copy_constuctor(content_, other.content_, index_); + } + + // Find the best match out of `T, Ts...` with `TArg` as the argument. + template + using best_match = decltype(overload_resolver()(std::declval())); + + template::type> //, typename=typename std::enable_if_t, variant>::value)>, typename TTarget=decltype(indicator_func(std::forward(std::declval()))), typename TIndex=index_of> + variant(TArg&& arg) { + new ((TTarget*)content_) TTarget{std::forward(arg)}; + index_ = index_of::value; + } + + ~variant() { + selective_destructor(content_, index_); + } + + inline variant& operator=(const variant & other) { + selective_destructor(content_, index_); + index_ = other.index_; + selective_copy_constuctor(content_, other.content_, index_); + return *this; + } + + inline bool operator==(const variant& rhs) const { + return (index_ == rhs.index_) && selective_eq(this->content_, rhs.content_, index_); + } + + inline bool operator!=(const variant& rhs) const { + return (index_ != rhs.index_) || selective_neq(this->content_, rhs.content_, index_); + } + + template + void invoke(TFunc functor, TArgs&&... args) const { + selective_invoke_const(content_, index_, functor, std::forward(args)...); + } + + template + void invoke(TFunc functor, TArgs&&... args) { + selective_invoke(content_, index_, functor, std::forward(args)...); + } + + storage_t content_; + size_t index_; + + size_t index() const { return index_; } +}; + +template +std::tuple_element_t>& get(std::variant& val) { + if (val.index() != I) + throw; + using T = std::tuple_element_t>; + return *((T*)val.content_); +} + +template +T& get(std::variant& val) { + constexpr size_t index = std::index_of::value; + return std::get(val); +} + +} // namespace std + +#endif + +/* Stuff that should be in the STL but isn't ---------------------------------*/ + +// source: https://en.cppreference.com/w/cpp/experimental/to_array +namespace detail { +template +constexpr std::array, N> + to_array_impl(T (&a)[N], std::index_sequence) +{ + return { {a[I]...} }; +} + +template +constexpr std::array, N> to_array(T (&a)[N]) +{ + return detail::to_array_impl(a, std::make_index_sequence{}); +} +} + + + +/* Custom utils --------------------------------------------------------------*/ + // @brief Supports various queries on a list of types template class TypeChecker; @@ -112,6 +487,7 @@ public: return std::is_base_of::value && TypeChecker::template all_are(); } + constexpr static const size_t count = TypeChecker::count + 1; }; template<> @@ -125,11 +501,17 @@ public: constexpr static inline bool all_are() { return std::true_type::value; } + constexpr static const size_t count = 0; }; +template +TypeChecker make_type_checker(Ts ...) { + return TypeChecker(); +} + #include #define ENABLE_IF(...) \ - typename = std::enable_if_t<__VA_ARGS__> + typename = typename std::enable_if_t<__VA_ARGS__> #define ENABLE_IF_SAME(a, b, type) \ template typename std::enable_if_t::value, type> @@ -151,15 +533,83 @@ class function_traits { public: template static TRet invoke(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std::tuple packed_args, TUnpackedArgs ... args) { - return invoke(obj, func_ptr, packed_args, args..., std::get(packed_args)); + return invoke(obj, func_ptr, packed_args, std::forward(args)..., std::get(packed_args)); } template static TRet invoke(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std::tuple packed_args, TArgs ... args) { - return (obj.*func_ptr)(args...); + return (obj.*func_ptr)(std::forward(args)...); } }; + +/* @brief return_type::type represents the C++ native return type +* of a function returning 0 or more arguments. +* +* For an empty TypeList, the return type is void. For a list with +* one type, the return type is equal to that type. For a list with +* more than one items, the return type is a tuple. +*/ +template +struct return_type; + +template<> +struct return_type<> { typedef void type; }; +template +struct return_type { typedef T type; }; +template +struct return_type { typedef std::tuple type; }; + + + +template +struct static_function_traits; + +// TODO: All invoke-related functions should be superseeded by a proper std::apply implementation +#if 0 +template +struct static_function_traits, std::tuple> { + using TRet = typename return_type::type; + + //template + //static std::tuple invoke(std::tuple packed_args, TUnpackedInputs ... args) { + // return invoke(packed_args, args..., std::get(packed_args)); + //} + + template + static std::tuple invoke(std::tuple& packed_args) { + return invoke_impl(packed_args, std::make_index_sequence()); + } + + template + static std::tuple invoke_impl(std::tuple packed_args, std::index_sequence) { + return invoke_impl_2(std::get(packed_args)...); + } + + //template + //static std::enable_if_t<(sizeof...(TOutputs) == 0), std::tuple> + template>*/> + static std::enable_if_t<(IOutputs == 0), std::tuple> + invoke_impl_2(TInputs ... args) { + Function(args...); + return std::make_tuple<>(); + } + + //template + //static std::enable_if_t<(sizeof...(TOutputs) == 1), std::tuple> + template>*/> + static std::enable_if_t<(IOutputs == 1), std::tuple> + invoke_impl_2(TInputs ... args) { + return std::make_tuple(Function(args...)); + } +// +// template= 2)> +// static /* std::enable_if_t= 2, */ std::tuple //> +// invoke_impl_2(std::tuple packed_args, TInputs ... args) { +// return Function(args...); +// } +}; + /* @brief Invoke a class member function with a variable number of arguments that are supplied as a tuple Example usage: @@ -180,4 +630,553 @@ TRet invoke_function_with_tuple(TObj& obj, TRet(TObj::*func_ptr)(TArgs...), std: return function_traits::template invoke<0>(obj, func_ptr, packed_args); } +template(*Function)(TIn...)> +std::tuple invoke_with_tuples(std::tuple inputs) { + static_function_traits::template invoke<0>(inputs); +} +#endif + + +template +struct sum_impl; +template +struct sum_impl { static constexpr TInt value = 0; }; +template +struct sum_impl { static constexpr TInt value = I + sum_impl::value; }; + +template +using sum = sum_impl; + + +// source: https://akrzemi1.wordpress.com/2017/05/18/asserts-in-constexpr-functions/ +#if defined NDEBUG +# define X_ASSERT(CHECK) void(0) +#else +# define X_ASSERT(CHECK) \ + ( (CHECK) ? void(0) : []{assert(!#CHECK);}() ) +#endif + +template +struct for_each_in_tuple_result_impl; + +template +struct for_each_in_tuple_result_impl> { + typedef std::tuple(std::declval())(std::get(std::declval())))...> type; +}; + +template +using for_each_in_tuple_result = for_each_in_tuple_result_impl>::value>>; + +template +using for_each_in_tuple_result_t = typename for_each_in_tuple_result::type; + +template +for_each_in_tuple_result_t for_each_in_tuple_impl(Fn&& f, Tuple&& t, std::index_sequence) { + return for_each_in_tuple_result_t(std::forward(f)(std::get(t))...); +} + +template +for_each_in_tuple_result_t for_each_in_tuple(Fn&& f, Tuple&& t) { + return for_each_in_tuple_impl(std::forward(f), std::forward(t), std::make_index_sequence>::value>{}); +} +//template +//for_each_in_tuple_result_t for_each_in_tuple(Fn&& f, Tuple&& t) { +// return 5; +//} + + +/* constexpr strings --------------------------------------------------------*/ +/* adapted from: +* https://akrzemi1.wordpress.com/2017/06/28/compile-time-string-concatenation/ +*/ + + +// TODO: the functionality +// sstring::substring, sstring::get_last_part and sstring::after_last_index_of and sstring::last_index_of +// was removed during refactoring. Add again if needed. + +/** + * @brief Represents a string that is known at compile time by encoding it as a + * type. + */ +template +struct sstring { + static constexpr const char chars[] = {CHARS..., 0}; + static constexpr const char* c_str() { return chars; } + static constexpr size_t size() { return sizeof...(CHARS); } + static constexpr std::array as_array() { return {CHARS...}; } + + template + constexpr bool operator==(const sstring & other) { + return as_array() == other.as_array(); + } +}; +template +constexpr const char sstring::chars[/*sizeof...(CHARS) + 1*/]; + +template +struct sstring_concat_impl; + +template +struct sstring_concat_impl, sstring> { + using type = sstring; +}; + +/** @brief Represents the result type of concatenating two static strings */ +template +using sstring_concat_t = typename sstring_concat_impl::type; + +/** @brief Concatenates two static strings */ +template +constexpr sstring operator+(sstring, sstring) { + return {}; +} + + +/** @brief Helper class for the MAKE_SSTRING macro */ +template +struct sstring_builder; + +template +struct sstring_builder<0, CHAR, CHARS...> { + using type = sstring<>; +}; + +template +struct sstring_builder { + using type = sstring_concat_t, typename sstring_builder::type>; +}; + +template +using sstring_builder_t = typename sstring_builder::type; + +#define MACRO_GET_1(str, i) \ + (sizeof(str) > (i) ? str[(i)] : 0) + +#define MACRO_GET_4(str, i) \ + MACRO_GET_1(str, i+0), \ + MACRO_GET_1(str, i+1), \ + MACRO_GET_1(str, i+2), \ + MACRO_GET_1(str, i+3) + +#define MACRO_GET_16(str, i) \ + MACRO_GET_4(str, i+0), \ + MACRO_GET_4(str, i+4), \ + MACRO_GET_4(str, i+8), \ + MACRO_GET_4(str, i+12) + +#define MACRO_GET_64(str, i) \ + MACRO_GET_16(str, i+0), \ + MACRO_GET_16(str, i+16), \ + MACRO_GET_16(str, i+32), \ + MACRO_GET_16(str, i+48) + +/** + * @brief Builds a compile-time string type from a string literal. + * + * Passing more than 64 characters will prune the string. + * + * Usage: + * MAKE_SSTRING("hello world") my_str{}; + * or + * auto my_str = MAKE_SSTRING("hello world"){}; + * + * Both examples create a compile-time variable "my_str" of which the type + * itself stores the content "hello world". + */ +#define MAKE_SSTRING(literal) sstring_builder_t + +namespace std { +template +static std::ostream& operator<<(std::ostream& stream, const sstring& val) { + stream << val.chars; + return stream; +} +} + +template +struct join_sstring_impl; + +template +struct join_sstring_impl> { + using type = sstring<>; +}; + +template +struct join_sstring_impl, sstring> { + using type = sstring; +}; + +template +struct join_sstring_impl, sstring, TStr...> { + using type = sstring_concat_t, typename join_sstring_impl, TStr...>::type>; +}; + +template +using join_sstring_t = typename join_sstring_impl::type; + +template +constexpr join_sstring_t join_sstring(const TDelimiter& delimiter, const TStr& ... str) { + return {}; +} + +template +using sstring_arr = std::tuple...>; + + +// source: https://stackoverflow.com/questions/40159732/return-other-value-if-key-not-found-in-the-map +template +TValue& get_or(std::unordered_map& m, const TKey& key, TValue& default_value) { + auto it = m.find(key); + if (it == m.end()) { + return default_value; + } else { + return it->second; + } +} +template +TValue* get_ptr(std::unordered_map& m, const TKey& key) { + auto it = m.find(key); + if (it == m.end()) + return nullptr; + else + return &(it->second); +} + +template +std::true_type is_complete_impl(T *); +std::false_type is_complete_impl(...); + +/** @brief is_complete resolves to std::true_type if T is complete + * and to std::false_type otherwise. This can be used to check if a certain template + * specialization exists. + **/ +template +using is_complete = decltype(is_complete_impl(std::declval())); + +template +struct dynamic_get_impl { + template + static TRet* get(size_t i, TTuple& t) { + if (i == I::value) + return &static_cast(std::get(t)); + else if (i > I::value) + return dynamic_get_impl, TRet, Ts...>::get(i, t); + return nullptr; // this should not happen + } +}; + +template +struct dynamic_get_impl, TRet, Ts...> { + static TRet* get(size_t i, const std::tuple& t) { + return nullptr; + } +}; + +template +TRet* dynamic_get(size_t i, std::tuple& t) { + return dynamic_get_impl, TRet, Ts...>::get(i, t); +} + +template +TRet* dynamic_get(size_t i, const std::tuple& t) { + return dynamic_get_impl, TRet, Ts...>::get(i, t); +} + + +template +class simple_iterator : std::iterator { + TDereferenceable *container_; + size_t i_; +public: + using reference = TResult; + explicit simple_iterator(TDereferenceable& container, size_t pos) : container_(&container), i_(pos) {} + simple_iterator& operator++() { ++i_; return *this; } + simple_iterator operator++(int) { simple_iterator retval = *this; ++(*this); return retval; } + bool operator==(simple_iterator other) const { return (container_ == other.container_) && (i_ == other.i_); } + bool operator!=(simple_iterator other) const { return !(*this == other); } + bool operator<(simple_iterator other) const { return i_ < other.i_; } + bool operator>(simple_iterator other) const { return i_ > other.i_; } + bool operator<=(simple_iterator other) const { return (*this < other) || (*this == other); } + bool operator>=(simple_iterator other) const { return (*this > other) || (*this == other); } + TResult operator*() const { return (*container_)[i_]; } +}; + + + +/** + * @brief Extracts the argument types of a function signature and provides them + * as a std::tuple. + * TODO: if an STL alternative exists, use that + */ +template +struct args_of; + +template +struct args_of { + using type = std::tuple; +}; + +template +struct args_of> { + using type = std::tuple; +}; + +template +struct args_of { + using type = std::tuple; +}; + +template +struct args_of : public args_of {}; + +template +using args_of_t = typename args_of::type; + +/** + * @brief Extracts the return type of a function signature + * + * This is provided because std::result_of is deprecated since C++17 + */ +template +struct result_of; + +template +struct result_of { + using type = TRet; +}; + +template +struct result_of { + using type = TRet; +}; + +template +struct result_of { + using type = TRet; +}; + +template +using result_of_t = typename result_of::type; + + +/** + * @brief Returns the type that results when concatenating multiple tuples + */ +template +using tuple_cat_t = decltype(std::tuple_cat(std::declval()...)); + +template +constexpr std::array array_cat_impl(std::array arr1, std::array arr2, std::index_sequence, std::index_sequence) { + return { arr1[PACK1]..., arr2[PACK2]... }; +} + +template +constexpr std::array array_cat(std::array arr1, std::array arr2) { + return array_cat_impl(arr1, arr2, std::make_index_sequence(), std::make_index_sequence()); +} + +/** + * @brief Returns the type that results when concatenating multiple tuples + */ +template +using tuple_cat_t = decltype(std::tuple_cat(std::declval()...)); + + +/** + * @brief Ensures that a given type is wrapped in a tuple + */ +template +struct as_tuple { + using type = std::tuple; +}; + +template<> +struct as_tuple { + using type = std::tuple<>; +}; + +template +struct as_tuple> { + using type = std::tuple; +}; + +template +using as_tuple_t = typename as_tuple::type; + +/** + * @brief Removes a reference OR pointer from the given type. + * + * This is similar to std::remove_reference, however it can also remove a + * pointer and it does not work for types that are neither a reference or + * a pointer. + */ +template +struct remove_ref_or_ptr { + static_assert(std::is_reference() || std::is_pointer(), "the type T is neither a reference or a pointer"); +}; + +template +struct remove_ref_or_ptr { using type = T; }; + +template +struct remove_ref_or_ptr { using type = T; }; + +template +using remove_ref_or_ptr_t = typename remove_ref_or_ptr::type; + +/** + * @brief Applies remove_ref_or_ptr_t to every type of a tuple type + */ +template +struct remove_refs_or_ptrs_from_tuple; + +template +struct remove_refs_or_ptrs_from_tuple> { + using type = std::tuple...>; +}; + +template +using remove_refs_or_ptrs_from_tuple_t = typename remove_refs_or_ptrs_from_tuple::type; + +/** + * @brief The convert(val) function returns a reference or a pointer to val + * depending on TTo. + * TODO: this could be a functor + */ +template +struct add_ref_or_ptr; + +template +struct add_ref_or_ptr { + static T& convert(T& value) { + return value; + } +}; + +template +struct add_ref_or_ptr { + static T* convert(T& value) { + return &value; + } +}; + + +/** + * @brief The convert() function turns a given tuple of values into a tuple of + * pointers or references based on the template argument TTo. + */ +template +struct add_ref_or_ptr_to_tuple; + +template +struct add_ref_or_ptr_to_tuple> { + template + static std::tuple convert_impl(std::tuple&& t, std::index_sequence) { + using to_type = std::tuple; + to_type result(add_ref_or_ptr>::convert(std::get(t))...); + return result; + } + + template + static std::tuple convert(std::tuple&& t) { + static_assert(sizeof...(TFrom) == sizeof...(TTo), "both tuples must have the same size"); + return convert_impl(std::forward>(t), std::make_index_sequence()); + } +}; + +template +struct add_ptrs_to_tuple_type; + +template +struct add_ptrs_to_tuple_type> { + using type = std::tuple; +}; + +template +using add_ptrs_to_tuple_t = typename add_ptrs_to_tuple_type::type; + +template +struct add_refs_to_tuple_type; + +template +struct add_refs_to_tuple_type> { + using type = std::tuple; +}; + +template +using add_refs_to_tuple_t = typename add_refs_to_tuple_type::type; + + +template struct is_tuple: std::false_type {}; +template struct is_tuple>: std::true_type {}; + + +template +struct tuple_select_type_impl; + +template +struct tuple_select_type_impl, TTuple> { + using type = std::tuple...>; +}; + +template +typename tuple_select_type_impl, TTuple>::type +tuple_select_impl(TTuple tuple, std::index_sequence) { + return typename tuple_select_type_impl, TTuple>::type(std::get(tuple)...); +}; + + +template +struct tuple_take_type { + static_assert(I <= std::tuple_size::value, "cannot take more elements than tuple size"); + using type = typename tuple_select_type_impl, TTuple>::type; +}; + +template +using tuple_take_t = typename tuple_take_type::type; + +/** + * @brief Returns the first I elements from the tuple as a tuple. + * The resulting type is tuple_take_t. + * See also: tuple_skip + */ +template +tuple_take_t tuple_take(TTuple tuple) { + return tuple_select_impl(tuple, std::make_index_sequence{}); +}; + + +template +struct tuple_skip_type { + static_assert(I <= std::tuple_size::value, "cannot skip more elements than tuple size"); + using type = typename tuple_select_type_impl::value>, TTuple>::type; +}; + +template +using tuple_skip_t = typename tuple_skip_type::type; + +/** + * @brief Returns all but the first I elements from the tuple as a tuple. + * The resulting type is tuple_skip_t. + * See also: tuple_take + */ +template +tuple_skip_t tuple_skip(TTuple tuple) { + return tuple_select_impl(tuple, fibre::make_integer_sequence_from_to::value>{}); +}; + +template +struct repeat_type_impl { + using type = typename repeat_type_impl::type; +}; + +template +struct repeat_type_impl<0, T, Ts...> { + using type = std::tuple; +}; + +template +using repeat_t = typename repeat_type_impl::type; + #endif // __CPP_UTILS_HPP diff --git a/Firmware/fibre/cpp/include/fibre/introspection.hpp b/Firmware/fibre/cpp/include/fibre/introspection.hpp new file mode 100644 index 00000000..f7e43f68 --- /dev/null +++ b/Firmware/fibre/cpp/include/fibre/introspection.hpp @@ -0,0 +1,219 @@ +#ifndef __FIBRE_INTROSPECTION_HPP +#define __FIBRE_INTROSPECTION_HPP + +#include +#include +#include + +#pragma GCC push_options +#pragma GCC optimize ("s") + +class TypeInfo; +class Introspectable; +using introspectable_storage_t = std::aligned_storage<16, 4>::type; + +struct PropertyInfo { + const char * name; + const TypeInfo* type_info; +}; + +/** + * @brief Contains runtime accessible type information. + * + * Specifically, this information consists of a list of PropertyInfo items which + * enable accessing attributes of an object by a runtime string. + * + * Typically, for each combination of C++ type and Fibre interface implemented + * by this type, one (static constant) TypeInfo object will exist. + */ +class TypeInfo { + friend class Introspectable; +public: + TypeInfo(const PropertyInfo* property_table, size_t property_table_length) + : property_table_(property_table), property_table_length_(property_table_length) {} + + virtual introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const = 0; + Introspectable get_child(const Introspectable& obj, const char * name, size_t length) const; + +protected: + + template static T& as(Introspectable& obj); + template static const T& as(const Introspectable& obj); + template static Introspectable make_introspectable(T obj, const TypeInfo* type_info); + +private: + const PropertyInfo* property_table_; + size_t property_table_length_; +}; + +/** + * @brief Wraps a reference to an application object by attaching runtime + * accessible type information. + * + * The reference that is wrapped is typically a pointer but can also be a small + * temporary, on-demand constructed object such as a fibre::Property<...> which + * contains multiple pointers. + */ +class Introspectable { + friend class TypeInfo; +public: + Introspectable() {} + + /** + * @brief Returns an Introspectable object for the attribute referenced by + * the specified attribute name. + * + * The name can consist of multiple parts separated by dots. + * + * If the attribute does not exist, an invalid Introspectable is returned. + * + * @param path: The name or path of the attribute. + * @param length: The maximum length of the name. + */ + Introspectable get_child(const char * path, size_t length) { + Introspectable current = *this; + + const char * begin = path; + const char * end = std::find(begin, path + length, '\0'); + + while ((begin < end) && current.type_info_) { + const char * end_of_token = std::find(begin, end, '.'); + current = current.get_direct_child(begin, end_of_token - begin); + begin = std::min(end, end_of_token + 1); + } + + return current; + }; + + bool is_valid() { + return type_info_; + } + + const TypeInfo* get_type_info() { + return type_info_; + } + +private: + Introspectable get_direct_child(const char * name, size_t length) const { + for (size_t i = 0; i < type_info_->property_table_length_; ++i) { + if (!strncmp(name, type_info_->property_table_[i].name, length) && (length == strlen(type_info_->property_table_[i].name))) { + Introspectable result; + result.storage_ = type_info_->get_child(storage_, i); + result.type_info_ = type_info_->property_table_[i].type_info; + return result; + } + } + return {}; + } + +public: // these should technically be protected but are public for optimization reasons + // We use this storage to hold generic small objects. Usually that's a pointer + // but sometimes it's an on-demand constructed Property<...>. + // Caution: only put objects in here which are trivially copyable, movable + // and destructible as any custom operation wouldn't be called. + introspectable_storage_t storage_; + const TypeInfo* type_info_ = nullptr; +}; + +template T& TypeInfo::as(Introspectable& obj) { + static_assert(sizeof(T) <= sizeof(obj.storage_)); + return *(T*)&obj.storage_; +} +template const T& TypeInfo::as(const Introspectable& obj) { + static_assert(sizeof(T) <= sizeof(obj.storage_)); + return *(const T*)&obj.storage_; +} +template Introspectable TypeInfo::make_introspectable(T obj, const TypeInfo* type_info) { + Introspectable introspectable; + as(introspectable) = obj; + introspectable.type_info_ = type_info; + return introspectable; +} + + +// maybe_underlying_type_t resolves to the underlying type of T if T is an enum type or otherwise to T itself. +template::value> struct maybe_underlying_type; +template struct maybe_underlying_type { typedef std::underlying_type_t type; }; +template struct maybe_underlying_type { typedef T type; }; +template using maybe_underlying_type_t = typename maybe_underlying_type::type; + + +struct StringConvertibleTypeInfo { + virtual bool get_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } + virtual bool set_string(const Introspectable& obj, char* buffer, size_t length) const { return false; } +}; + +struct FloatSettableTypeInfo { + //virtual bool get_float(const Introspectable& obj, float* val) const { return false; } + virtual bool set_float(const Introspectable& obj, float val) const { return false; } +}; + +/* Built-in type infos ********************************************************/ + +template +struct FibrePropertyTypeInfo; + +// readonly property +template +struct FibrePropertyTypeInfo> : StringConvertibleTypeInfo, TypeInfo { + using TypeInfo::TypeInfo; + static const PropertyInfo property_table[]; + static const FibrePropertyTypeInfo> singleton; + + introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const override { + return {}; + } + + bool get_string(const Introspectable& obj, char* buffer, size_t length) const override { + return to_string(static_cast>(as>(obj).read()), buffer, length, 0); + } +}; + +template +const PropertyInfo FibrePropertyTypeInfo>::property_table[] = {}; +template +const FibrePropertyTypeInfo> FibrePropertyTypeInfo>::singleton{FibrePropertyTypeInfo>::property_table, sizeof(FibrePropertyTypeInfo>::property_table) / sizeof(FibrePropertyTypeInfo>::property_table[0])}; + +// readwrite property +template +struct FibrePropertyTypeInfo> : FloatSettableTypeInfo, StringConvertibleTypeInfo, TypeInfo { + using TypeInfo::TypeInfo; + static const PropertyInfo property_table[]; + static const FibrePropertyTypeInfo> singleton; + static const Introspectable make_introspectable(Property obj) { return TypeInfo::make_introspectable(obj, &singleton); } + + introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const override { + return {}; + } + + bool get_string(const Introspectable& obj, char* buffer, size_t length) const override { + return to_string(static_cast>(as>(obj).read()), buffer, length, 0); + } + + bool set_string(const Introspectable& obj, char* buffer, size_t length) const override { + maybe_underlying_type_t value; + if (!from_string(buffer, length, &value, 0)) { + return false; + } + as>(obj).exchange(static_cast(value)); + return true; + } + + bool set_float(const Introspectable& obj, float val) const override { + maybe_underlying_type_t value; + if (!conversion::set_from_float(val, &value)) { + return false; + } + as>(obj).exchange(static_cast(value)); + return true; + } +}; + +template +const PropertyInfo FibrePropertyTypeInfo>::property_table[] = {}; +template +const FibrePropertyTypeInfo> FibrePropertyTypeInfo>::singleton{FibrePropertyTypeInfo>::property_table, sizeof(FibrePropertyTypeInfo>::property_table) / sizeof(FibrePropertyTypeInfo>::property_table[0])}; + +#pragma GCC pop_options + +#endif // __FIBRE_INTROSPECTION_HPP \ No newline at end of file diff --git a/Firmware/fibre/cpp/include/fibre/protocol.hpp b/Firmware/fibre/cpp/include/fibre/protocol.hpp index c1f3cc68..2a01e220 100644 --- a/Firmware/fibre/cpp/include/fibre/protocol.hpp +++ b/Firmware/fibre/cpp/include/fibre/protocol.hpp @@ -12,9 +12,14 @@ see protocol.md for the protocol specification #include #include //#include +#include #include +#include +#include #include "crc.hpp" #include "cpp_utils.hpp" +#include "bufptr.hpp" +#include "simple_serdes.hpp" // Note that this option cannot be used to debug UART because it prints on UART //#define DEBUG_FIBRE @@ -63,9 +68,6 @@ struct ReceiverState { /*******************************************************/ - -#include - constexpr uint16_t PROTOCOL_VERSION = 1; // This value must not be larger than USB_TX_DATA_SIZE defined in usbd_cdc_if.h @@ -77,12 +79,23 @@ constexpr uint32_t PROTOCOL_SERVER_TIMEOUT_MS = 10; typedef struct { - uint16_t json_crc; - uint16_t node_id; - uint16_t endpoint_id; + uint16_t json_crc = 0; + uint16_t endpoint_id = 0; } endpoint_ref_t; -#include + +namespace fibre { +// These symbols are defined in the autogenerated endpoints.hpp +extern const unsigned char embedded_json[]; +extern const size_t embedded_json_length; +extern const uint16_t json_crc_; +extern const uint32_t json_version_id_; +bool endpoint_handler(int idx, cbufptr_t* input_buffer, bufptr_t* output_buffer); +bool endpoint0_handler(cbufptr_t* input_buffer, bufptr_t* output_buffer); +bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref); +bool set_endpoint_from_float(endpoint_ref_t endpoint_ref, float value); +} + template::value>> inline size_t write_le(T value, uint8_t* buffer){ @@ -101,8 +114,9 @@ template<> inline size_t write_le(float value, uint8_t* buffer) { static_assert(CHAR_BIT * sizeof(float) == 32, "32 bit floating point expected"); static_assert(std::numeric_limits::is_iec559, "IEEE 754 floating point expected"); - const uint32_t * value_as_uint32 = reinterpret_cast(&value); - return write_le(*value_as_uint32, buffer); + uint32_t value_as_uint32; + std::memcpy(&value_as_uint32, &value, sizeof(uint32_t)); + return write_le(value_as_uint32, buffer); } template @@ -116,7 +130,6 @@ template<> inline size_t read_le(float* value, const uint8_t* buffer) { static_assert(CHAR_BIT * sizeof(float) == 32, "32 bit floating point expected"); static_assert(std::numeric_limits::is_iec559, "IEEE 754 floating point expected"); - return read_le(reinterpret_cast(value), buffer); } @@ -183,19 +196,19 @@ public: class StreamToPacketSegmenter : public StreamSink { public: - StreamToPacketSegmenter(PacketSink& output) : + explicit StreamToPacketSegmenter(PacketSink& output) : output_(output) { }; - int process_bytes(const uint8_t *buffer, size_t length, size_t* processed_bytes); + int process_bytes(const uint8_t *buffer, size_t length, size_t* processed_bytes) override; size_t get_free_space() { return SIZE_MAX; } private: - uint8_t header_buffer_[3]; + uint8_t header_buffer_[3] = {0}; size_t header_index_ = 0; - uint8_t packet_buffer_[RX_BUF_SIZE]; + uint8_t packet_buffer_[RX_BUF_SIZE] = {0}; size_t packet_index_ = 0; size_t packet_length_ = 0; PacketSink& output_; @@ -204,13 +217,13 @@ private: class StreamBasedPacketSink : public PacketSink { public: - StreamBasedPacketSink(StreamSink& output) : + explicit StreamBasedPacketSink(StreamSink& output) : output_(output) { }; //size_t get_mtu() { return SIZE_MAX; } - int process_packet(const uint8_t *buffer, size_t length); + int process_packet(const uint8_t *buffer, size_t length) override; private: StreamSink& output_; @@ -220,10 +233,10 @@ private: // A single call to process_bytes may result in multiple packets being sent. class PacketBasedStreamSink : public StreamSink { public: - PacketBasedStreamSink(PacketSink& packet_sink) : _packet_sink(packet_sink) {} + explicit PacketBasedStreamSink(PacketSink& packet_sink) : _packet_sink(packet_sink) {} ~PacketBasedStreamSink() {} - int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) { + int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) override { // Loop to ensure all bytes get sent while (length) { size_t chunk = length; @@ -253,7 +266,7 @@ public: buffer_length_(length) {} // Returns 0 on success and -1 if the buffer could not accept everything because it became full - int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) { + int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) override { size_t chunk = length < buffer_length_ ? length : buffer_length_; memcpy(buffer_, buffer, chunk); buffer_ += chunk; @@ -279,7 +292,7 @@ public: follow_up_stream_(follow_up_stream) {} // Returns 0 on success and -1 if the buffer could not accept everything because it became full - int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) { + int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) override { if (skip_ < length) { buffer += skip_; length -= skip_; @@ -295,7 +308,7 @@ public: } } - size_t get_free_space() { return skip_ + follow_up_stream_.get_free_space(); } + size_t get_free_space() override { return skip_ + follow_up_stream_.get_free_space(); } private: size_t skip_; @@ -308,17 +321,17 @@ private: // on the data that is sent to it. class CRC16Calculator : public StreamSink { public: - CRC16Calculator(uint16_t crc16_init) : + explicit CRC16Calculator(uint16_t crc16_init) : crc16_(crc16_init) {} - int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) { + int process_bytes(const uint8_t* buffer, size_t length, size_t* processed_bytes) override{ crc16_ = calc_crc16(crc16_, buffer, length); if (processed_bytes) *processed_bytes += length; return 0; } - size_t get_free_space() { return SIZE_MAX; } + size_t get_free_space() override { return SIZE_MAX; } uint16_t get_crc16() { return crc16_; } private: @@ -326,160 +339,77 @@ private: }; -// @brief Endpoint request handler -// -// When passed a valid endpoint context, implementing functions shall handle an -// endpoint read/write request by reading the provided input data and filling in -// output data. The exact semantics of this function depends on the corresponding -// endpoint's specification. -// -// @param input: pointer to the input data -// @param input_length: number of available input bytes -// @param output: The stream where to write the output to. Can be null. -// The handler shall abort as soon as the stream returns -// a non-zero error code on write. -typedef std::function EndpointHandler; - - -// @brief Default endpoint handler for const types -// @return: True if endpoint was written to, False otherwise -template -std::enable_if_t::value && std::is_const::value, bool> -default_readwrite_endpoint_handler(T* value, const uint8_t* input, size_t input_length, StreamSink* output) { - // If the old value was requested, call the corresponding little endian serialization function - if (output) { - // TODO: make buffer size dependent on the type - uint8_t buffer[sizeof(T)]; - size_t cnt = write_le(*value, buffer); - if (cnt <= output->get_free_space()) - output->process_bytes(buffer, cnt, nullptr); - } - return false; // We don't ever write to const types -} - -// @brief Default endpoint handler for non-const types -template -std::enable_if_t::value && !std::is_const::value, bool> -default_readwrite_endpoint_handler(T* value, const uint8_t* input, size_t input_length, StreamSink* output) { - // Read the endpoint value into output - default_readwrite_endpoint_handler(const_cast(value), input, input_length, output); - - // If a new value was passed, call the corresponding little endian deserialization function - uint8_t buffer[sizeof(T)] = { 0 }; // TODO: make buffer size dependent on the type - if (input_length >= sizeof(buffer)) { - read_le(value, input); - return true; - } else { - return false; - } -} - -// @brief Default endpoint handler for endpoint_ref_t types -template -bool default_readwrite_endpoint_handler(endpoint_ref_t* value, const uint8_t* input, size_t input_length, StreamSink* output) { - constexpr size_t size = sizeof(value->endpoint_id) + sizeof(value->json_crc); - if (output) { - // TODO: make buffer size dependent on the type - uint8_t buffer[size]; - size_t cnt = write_leendpoint_id)>(value->endpoint_id, buffer); - cnt += write_lejson_crc)>(value->json_crc, buffer + cnt); - if (cnt <= output->get_free_space()) - output->process_bytes(buffer, cnt, nullptr); - } - - // If a new value was passed, call the corresponding little endian deserialization function - if (input_length >= size) { - read_leendpoint_id)>(&value->endpoint_id, input); - read_lejson_crc)>(&value->json_crc, input + 2); - return true; - } else { - return false; - } -} - -template -static inline const char* get_default_json_modifier(); - -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"float\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"float\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int64\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int64\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint64\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint64\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int32\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"int32\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint32\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint32\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint16\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint16\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint8\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"uint8\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"bool\",\"access\":\"r\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"bool\",\"access\":\"rw\""; -} -template<> -inline constexpr const char* get_default_json_modifier() { - return "\"type\":\"endpoint_ref\",\"access\":\"rw\""; -} - -class Endpoint { -public: - //const char* const name_; - virtual void handle(const uint8_t* input, size_t input_length, StreamSink* output) = 0; - virtual bool get_string(char * output, size_t length) { return false; } - virtual bool set_string(char * buffer, size_t length) { return false; } - virtual bool set_from_float(float value) { return false; } +namespace fibre { +template +struct Codec { + static std::optional decode(cbufptr_t* buffer) { return std::nullopt; } }; -static inline int write_string(const char* str, StreamSink* output) { - return output->process_bytes(reinterpret_cast(str), strlen(str), nullptr); +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return (buffer->begin() == buffer->end()) ? std::nullopt : std::make_optional((bool)*(buffer->begin()++)); } + static bool encode(bool value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int8_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint8_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int16_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint16_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int32_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint32_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(int64_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { return SimpleSerializer::read(&(buffer->begin()), buffer->end()); } + static bool encode(uint64_t value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { + std::optional int_val = Codec::decode(buffer); + return int_val.has_value() ? std::optional(*reinterpret_cast(&int_val.value())) : std::nullopt; + } + static bool encode(float value, bufptr_t* buffer) { + void* ptr = &value; + return Codec::encode(*reinterpret_cast(ptr), buffer); + } +}; +template +struct Codec::value>> { + static std::optional decode(cbufptr_t* buffer) { + std::optional int_val = SimpleSerializer::read(&(buffer->begin()), buffer->end()); + return int_val.has_value() ? std::make_optional(static_cast(int_val.value())) : std::nullopt; + } + static bool encode(T value, bufptr_t* buffer) { return SimpleSerializer::write(value, &(buffer->begin()), buffer->end()); } +}; +template<> struct Codec { + static std::optional decode(cbufptr_t* buffer) { + std::optional val0 = SimpleSerializer::read(&(buffer->begin()), buffer->end()); + std::optional val1 = SimpleSerializer::read(&(buffer->begin()), buffer->end()); + return (val0.has_value() && val1.has_value()) ? std::make_optional(endpoint_ref_t{val1.value(), val0.value()}) : std::nullopt; + } + static bool encode(endpoint_ref_t value, bufptr_t* buffer) { + return SimpleSerializer::write(value.endpoint_id, &(buffer->begin()), buffer->end()) + && SimpleSerializer::write(value.json_crc, &(buffer->begin()), buffer->end()); + } +}; } @@ -492,17 +422,17 @@ static inline int write_string(const char* str, StreamSink* output) { */ class BidirectionalPacketBasedChannel : public PacketSink { public: - BidirectionalPacketBasedChannel(PacketSink& output) : + explicit BidirectionalPacketBasedChannel(PacketSink& output) : output_(output) { } //size_t get_mtu() { // return SIZE_MAX; //} - int process_packet(const uint8_t* buffer, size_t length); + int process_packet(const uint8_t* buffer, size_t length) override; private: PacketSink& output_; - uint8_t tx_buf_[TX_BUF_SIZE]; + uint8_t tx_buf_[TX_BUF_SIZE] = {0}; }; @@ -537,6 +467,11 @@ template<> struct format_traits_t { using type = void; static constexpr const char * fmt = "%lu"; static constexpr const char * fmtp = "%lu"; }; +// TODO: change all overloads to fundamental int type space +template<> struct format_traits_t { using type = void; + static constexpr const char * fmt = "%ud"; + static constexpr const char * fmtp = "%ud"; +}; template<> struct format_traits_t { using type = void; static constexpr const char * fmt = "%hd"; static constexpr const char * fmtp = "%hd"; @@ -578,7 +513,17 @@ static bool to_string(const T& value, char * buffer, size_t length, ...) { template::type> static bool from_string(const char * buffer, size_t length, T* property, int) { - return sscanf(buffer, format_traits_t::fmt, property) == 1; + // Note for T == uint8_t: Even though we supposedly use the correct format + // string sscanf treats our pointer as pointer-to-int instead of + // pointer-to-uint8_t. To avoid an unexpected memory access we first read + // into a union. + union { T t; int i; } val; + if (sscanf(buffer, format_traits_t::fmt, &val.t) == 1) { + *property = val.t; + return true; + } else { + return false; + } } // Special case for float because printf promotes float to double, and we get warnings template @@ -599,112 +544,6 @@ static bool from_string(const char * buffer, size_t length, T* property, ...) { } -/* Object tree ---------------------------------------------------------------*/ - -template -struct MemberList; - -template<> -struct MemberList<> { -public: - static constexpr size_t endpoint_count = 0; - static constexpr bool is_empty = true; - void write_json(size_t id, StreamSink* output) { - // no action - } - void register_endpoints(Endpoint** list, size_t id, size_t length) { - // no action - } - Endpoint* get_by_name(const char * name, size_t length) { - return nullptr; - } - std::tuple<> get_names_as_tuple() const { return std::tuple<>(); } -}; - -template -struct MemberList { -public: - static constexpr size_t endpoint_count = TMember::endpoint_count + MemberList::endpoint_count; - static constexpr bool is_empty = false; - - MemberList(TMember&& this_member, TMembers&&... subsequent_members) : - this_member_(std::forward(this_member)), - subsequent_members_(std::forward(subsequent_members)...) {} - - MemberList(TMember&& this_member, MemberList&& subsequent_members) : - this_member_(std::forward(this_member)), - subsequent_members_(std::forward>(subsequent_members)) {} - - // @brief Move constructor -/* MemberList(MemberList&& other) : - this_member_(std::move(other.this_member_)), - subsequent_members_(std::move(other.subsequent_members_)) {}*/ - - void write_json(size_t id, StreamSink* output) /*final*/ { - this_member_.write_json(id, output); - if (!MemberList::is_empty) - write_string(",", output); - subsequent_members_.write_json(id + TMember::endpoint_count, output); - } - - Endpoint* get_by_name(const char * name, size_t length) { - Endpoint* result = this_member_.get_by_name(name, length); - if (result) return result; - else return subsequent_members_.get_by_name(name, length); - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) /*final*/ { - this_member_.register_endpoints(list, id, length); - subsequent_members_.register_endpoints(list, id + TMember::endpoint_count, length); - } - - TMember this_member_; - MemberList subsequent_members_; -}; - -template -MemberList make_protocol_member_list(TMembers&&... member_list) { - return MemberList(std::forward(member_list)...); -} - -template -class ProtocolObject { -public: - ProtocolObject(const char * name, TMembers&&... member_list) : - name_(name), - member_list_(std::forward(member_list)...) {} - - static constexpr size_t endpoint_count = MemberList::endpoint_count; - - void write_json(size_t id, StreamSink* output) { - write_string("{\"name\":\"", output); - write_string(name_, output); - write_string("\",\"type\":\"object\",\"members\":[", output); - member_list_.write_json(id, output), - write_string("]}", output); - } - - Endpoint* get_by_name(const char * name, size_t length) { - size_t segment_length = strlen(name); - if (!strncmp(name, name_, length)) - return member_list_.get_by_name(name + segment_length + 1, length - segment_length - 1); - else - return nullptr; - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) { - member_list_.register_endpoints(list, id, length); - } - - const char * name_; - MemberList member_list_; -}; - -template -ProtocolObject make_protocol_object(const char * name, TMembers&&... member_list) { - return ProtocolObject(name, std::forward(member_list)...); -} - //template //bool set_from_float_ex(float value, T* property) { // return false; @@ -734,389 +573,49 @@ bool set_from_float(float value, T* property) { } } -//template -//bool set_from_float_ex<>(float value, T* property) { -// return false; -//} +template +struct Property { + Property(void* ctx, T(*getter)(void*), void(*setter)(void*, T)) + : ctx_(ctx), getter_(getter), setter_(setter) {} + Property(T* ctx) + : ctx_(ctx), getter_([](void* ctx){ return *(T*)ctx; }), setter_([](void* ctx, T val){ *(T*)ctx = val; }) {} + Property& operator*() { return *this; } + Property* operator->() { return this; } -template -class ProtocolProperty : public Endpoint { -public: - static constexpr const char * json_modifier = get_default_json_modifier(); - static constexpr size_t endpoint_count = 1; - - ProtocolProperty(const char * name, TProperty* property, - void (*written_hook)(void*), void* ctx) - : name_(name), property_(property), written_hook_(written_hook), ctx_(ctx) - {} - -/* TODO: find out why the move constructor is not used when it could be - ProtocolProperty(const ProtocolProperty&) = delete; - // @brief Move constructor - ProtocolProperty(ProtocolProperty&& other) : - Endpoint(std::move(other)), - name_(std::move(other.name_)), - property_(other.property_) - {} - constexpr ProtocolProperty& operator=(const ProtocolProperty& other) = delete; - constexpr ProtocolProperty& operator=(const ProtocolProperty& other) { - //Endpoint(std::move(other)), - //name_(std::move(other.name_)), - //property_(other.property_) - name_ = other.name_; - property_ = other.property_; - return *this; + T read() const { + return (*getter_)(ctx_); } - ProtocolProperty& operator=(ProtocolProperty&& other) - : name_(other.name_), property_(other.property_) - {} - ProtocolProperty& operator=(const ProtocolProperty& other) - : name_(other.name_), property_(other.property_) - {}*/ - void write_json(size_t id, StreamSink* output) { - // write name - write_string("{\"name\":\"", output); - LOG_FIBRE("json: this at %x, name at %x is s\r\n", (uintptr_t)this, (uintptr_t)name_); - //LOG_FIBRE("json\r\n"); - write_string(name_, output); - - // write endpoint ID - write_string("\",\"id\":", output); - char id_buf[10]; - snprintf(id_buf, sizeof(id_buf), "%u", (unsigned)id); // TODO: get rid of printf - write_string(id_buf, output); - - // write additional JSON data - if (json_modifier && json_modifier[0]) { - write_string(",", output); - write_string(json_modifier, output); + T exchange(std::optional value) const { + T old_value = (*getter_)(ctx_); + if (value.has_value()) { + (*setter_)(ctx_, value.value()); } - - write_string("}", output); + return old_value; } - - // special-purpose function - to be moved - Endpoint* get_by_name(const char * name, size_t length) { - if (!strncmp(name, name_, length)) - return this; - else - return nullptr; - } - - // special-purpose function - to be moved - bool get_string(char * buffer, size_t length) final { - return to_string(*property_, buffer, length, 0); - } - - // special-purpose function - to be moved - bool set_string(char * buffer, size_t length) final { - return from_string(buffer, length, property_, 0); - } - - bool set_from_float(float value) final { - return conversion::set_from_float(value, property_); - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) { - if (id < length) - list[id] = this; - } - void handle(const uint8_t* input, size_t input_length, StreamSink* output) final { - bool wrote = default_readwrite_endpoint_handler(property_, input, input_length, output); - if (wrote && written_hook_ != nullptr) { - written_hook_(ctx_); - } - } - /*void handle(const uint8_t* input, size_t input_length, StreamSink* output) { - handle(input, input_length, output); - }*/ - - const char* name_; - TProperty* property_; - void (*written_hook_)(void*); + void* ctx_; -}; - -// Non-const non-enum types -template::value)> -ProtocolProperty make_protocol_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty(name, property, written_hook, ctx); -}; - -// Const non-enum types -template::value)> -ProtocolProperty make_protocol_ro_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty(name, property, written_hook, ctx); -}; - -// Non-const enum types -template::value)> -ProtocolProperty> make_protocol_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty>( - name, reinterpret_cast*>(property), written_hook, ctx); -}; - -// Const enum types -template::value)> -ProtocolProperty> make_protocol_ro_property(const char * name, TProperty* property, - void (*written_hook)(void*) = nullptr, void* ctx = nullptr) { - return ProtocolProperty>( - name, reinterpret_cast*>(property), written_hook, ctx); -}; - - -template -struct PropertyListFactory; - -template<> -struct PropertyListFactory<> { - template - static MemberList<> make_property_list(std::array names, std::tuple& values) { - return MemberList<>(); - } -}; - -template -struct PropertyListFactory { - template - static MemberList, ProtocolProperty...> - make_property_list(std::array names, std::tuple& values) { - return MemberList, ProtocolProperty...>( - make_protocol_property(std::get(names), &std::get(values)), - PropertyListFactory::template make_property_list(names, values) - ); - } -}; - -/* @brief return_type::type represents the true return type -* of a function returning 0 or more arguments. -* -* For an empty TypeList, the return type is void. For a list with -* one type, the return type is equal to that type. For a list with -* more than one items, the return type is a tuple. -*/ -template -struct return_type; - -template<> -struct return_type<> { typedef void type; }; -template -struct return_type { typedef T type; }; -template -struct return_type { typedef std::tuple type; }; - - -template -class ProtocolFunction; - -template -class ProtocolFunction, std::tuple> : Endpoint { -public: - // @brief The return type of the function as written by a C++ programmer - using TRet = typename return_type::type; - - static constexpr size_t endpoint_count = 1 + MemberList...>::endpoint_count + MemberList...>::endpoint_count; - - ProtocolFunction(const char * name, TObj& obj, TRet(TObj::*func_ptr)(TInputs...), - std::array input_names, - std::array output_names) : - name_(name), obj_(&obj), func_ptr_(func_ptr), - input_names_{input_names}, output_names_{output_names}, - input_properties_(PropertyListFactory::template make_property_list<0>(input_names_, in_args_)), - output_properties_(PropertyListFactory::template make_property_list<0>(output_names_, out_args_)) - { - LOG_FIBRE("my tuple is at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); - } - - // The custom copy constructor is needed because otherwise the - // input_properties_ and output_properties_ would point to memory - // locations of the old object. - ProtocolFunction(const ProtocolFunction& other) : - name_(other.name_), obj_(other.obj_), func_ptr_(other.func_ptr_), - input_names_{other.input_names_}, output_names_{other.output_names_}, - input_properties_(PropertyListFactory::template make_property_list<0>(input_names_, in_args_)), - output_properties_(PropertyListFactory::template make_property_list<0>(output_names_, out_args_)) - { - LOG_FIBRE("COPIED! my tuple is at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); - } - - void write_json(size_t id, StreamSink* output) { - // write name - write_string("{\"name\":\"", output); - write_string(name_, output); - - // write endpoint ID - write_string("\",\"id\":", output); - char id_buf[10]; - snprintf(id_buf, sizeof(id_buf), "%u", (unsigned)id); // TODO: get rid of printf - write_string(id_buf, output); - - // write arguments - write_string(",\"type\":\"function\",\"inputs\":[", output); - input_properties_.write_json(id + 1, output), - write_string("],\"outputs\":[", output); - output_properties_.write_json(id + 1 + decltype(input_properties_)::endpoint_count, output), - write_string("]}", output); - } - - // special-purpose function - to be moved - Endpoint* get_by_name(const char * name, size_t length) { - return nullptr; // can't address functions by name - } - - void register_endpoints(Endpoint** list, size_t id, size_t length) { - if (id < length) - list[id] = this; - input_properties_.register_endpoints(list, id + 1, length); - output_properties_.register_endpoints(list, id + 1 + decltype(input_properties_)::endpoint_count, length); - } - - template std::enable_if_t - handle_ex() { - invoke_function_with_tuple(*obj_, func_ptr_, in_args_); - } - - template std::enable_if_t - handle_ex() { - std::get<0>(out_args_) = invoke_function_with_tuple(*obj_, func_ptr_, in_args_); - } - - template std::enable_if_t= 2> - handle_ex() { - out_args_ = invoke_function_with_tuple(*obj_, func_ptr_, in_args_); - } - - void handle(const uint8_t* input, size_t input_length, StreamSink* output) final { - (void) input; - (void) input_length; - (void) output; - LOG_FIBRE("tuple still at %x and of size %u\r\n", (uintptr_t)&in_args_, sizeof(in_args_)); - LOG_FIBRE("invoke function using %d and %.3f\r\n", std::get<0>(in_args_), std::get<1>(in_args_)); - handle_ex(); - } - - const char * name_; - TObj* obj_; - TRet(TObj::*func_ptr_)(TInputs...); - std::array input_names_; // TODO: remove - std::array output_names_; // TODO: remove - std::tuple in_args_; - std::tuple out_args_; - MemberList...> input_properties_; - MemberList...> output_properties_; -}; - -template> -ProtocolFunction, std::tuple<>> make_protocol_function(const char * name, TObj& obj, void(TObj::*func_ptr)(TArgs...), TNames ... names) { - return ProtocolFunction, std::tuple<>>(name, obj, func_ptr, {names...}, {}); -} - -template::value>> -ProtocolFunction, std::tuple> make_protocol_function(const char * name, TObj& obj, TRet(TObj::*func_ptr)(TArgs...), TNames ... names) { - return ProtocolFunction, std::tuple>(name, obj, func_ptr, {names...}, {"result"}); -} - - -#define FIBRE_EXPORTS(CLASS, ...) \ - struct fibre_export_t { \ - static CLASS* obj; \ - using type = decltype(make_protocol_member_list(__VA_ARGS__)); \ - }; \ - fibre_export_t::type make_fibre_definitions() { \ - CLASS* obj = this; \ - return make_protocol_member_list(__VA_ARGS__); \ - } \ - fibre_export_t::type fibre_definitions = make_fibre_definitions() - - - - - -class EndpointProvider { -public: - virtual size_t get_endpoint_count() = 0; - virtual void write_json(size_t id, StreamSink* output) = 0; - virtual Endpoint* get_by_name(char * name, size_t length) = 0; - virtual void register_endpoints(Endpoint** list, size_t id, size_t length) = 0; + T(*getter_)(void*); + void(*setter_)(void*, T); }; template -class EndpointProvider_from_MemberList : public EndpointProvider { -public: - EndpointProvider_from_MemberList(T& member_list) : member_list_(member_list) {} - size_t get_endpoint_count() final { - return T::endpoint_count; +struct Property { + Property(void* ctx, T(*getter)(void*)) + : ctx_(ctx), getter_(getter) {} + Property(const T* ctx) + : ctx_(const_cast(ctx)), getter_([](void* ctx){ return *(const T*)ctx; }) {} + Property& operator*() { return *this; } + Property* operator->() { return this; } + + T read() const { + return (*getter_)(ctx_); } - void write_json(size_t id, StreamSink* output) final { - return member_list_.write_json(id, output); - } - void register_endpoints(Endpoint** list, size_t id, size_t length) final { - return member_list_.register_endpoints(list, id, length); - } - Endpoint* get_by_name(char * name, size_t length) final { - for (size_t i = 0; i < length; i++) { - if (name[i] == '.') - name[i] = 0; - } - name[length-1] = 0; - return member_list_.get_by_name(name, length); - } - T& member_list_; + + void* ctx_; + T(*getter_)(void*); }; - -class JSONDescriptorEndpoint : Endpoint { -public: - static constexpr size_t endpoint_count = 1; - void write_json(size_t id, StreamSink* output); - void register_endpoints(Endpoint** list, size_t id, size_t length); - void handle(const uint8_t* input, size_t input_length, StreamSink* output); -}; - -// defined in protocol.cpp -extern Endpoint** endpoint_list_; -extern size_t n_endpoints_; -extern uint16_t json_crc_; -extern JSONDescriptorEndpoint json_file_endpoint_; -extern EndpointProvider* application_endpoints_; - -bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref); -Endpoint* get_endpoint(endpoint_ref_t endpoint_ref); - -// @brief Registers the specified application object list using the provided endpoint table. -// This function should only be called once during the lifetime of the application. TODO: fix this. -// @param application_objects The application objects to be registred. -template -int fibre_publish(T& application_objects) { - static constexpr size_t endpoint_list_size = 1 + T::endpoint_count; - static Endpoint* endpoint_list[endpoint_list_size]; - static auto endpoint_provider = EndpointProvider_from_MemberList(application_objects); - - json_file_endpoint_.register_endpoints(endpoint_list, 0, endpoint_list_size); - application_objects.register_endpoints(endpoint_list, 1, endpoint_list_size); - - // Update the global endpoint table - endpoint_list_ = endpoint_list; - n_endpoints_ = endpoint_list_size; - application_endpoints_ = &endpoint_provider; - - // Calculate the CRC16 of the JSON file. - // The init value is the protocol version. - CRC16Calculator crc16_calculator(PROTOCOL_VERSION); - uint8_t offset[4] = { 0 }; - json_file_endpoint_.handle(offset, sizeof(offset), &crc16_calculator); - json_crc_ = crc16_calculator.get_crc16(); - - return 0; -} - - #endif diff --git a/Firmware/fibre/cpp/include/fibre/simple_serdes.hpp b/Firmware/fibre/cpp/include/fibre/simple_serdes.hpp new file mode 100644 index 00000000..32c09f27 --- /dev/null +++ b/Firmware/fibre/cpp/include/fibre/simple_serdes.hpp @@ -0,0 +1,77 @@ +#ifndef __FIBRE_SIMPLE_SERDES +#define __FIBRE_SIMPLE_SERDES + +//#include "stream.hpp" + + +template +struct SimpleSerializer; +template +using LittleEndianSerializer = SimpleSerializer; +template +using BigEndianSerializer = SimpleSerializer; + + +/* @brief Serializer/deserializer for arbitrary integral number types */ +// TODO: allow reading an arbitrary number of bits +template +struct SimpleSerializer::value>> { + static constexpr size_t BIT_WIDTH = std::numeric_limits::digits; + static constexpr size_t BYTE_WIDTH = (BIT_WIDTH + 7) / 8; + + template + static std::optional read(TIterator* begin, TIterator end = nullptr) { + T result = 0; + if (BigEndian) { + for (size_t i = BYTE_WIDTH; i > 0; (i++, (*begin)++)) { + if (end && !(*begin < end)) + return std::nullopt; + uint8_t byte = **begin; + result |= static_cast(byte) << ((i - 1) << 3); + } + } else { + for (size_t i = 0; i < BYTE_WIDTH; (i++, (*begin)++)) { + if (end && !(*begin < end)) + return std::nullopt; + uint8_t byte = **begin; + result |= static_cast(byte) << (i << 3); + } + } + return result; + } + + template + static bool write(T value, TIterator* begin, TIterator end = nullptr) { + if (BigEndian) { + for (size_t i = BYTE_WIDTH; i > 0; (i--, (*begin)++)) { + if (end && !(*begin < end)) + return false; + uint8_t byte = static_cast((value >> ((i - 1) << 3)) & 0xff); + **begin = byte; + } + } else { + for (size_t i = 0; i < BYTE_WIDTH; (i++, (*begin)++)) { + if (end && !(*begin < end)) + return false; + uint8_t byte = static_cast((value >> (i << 3)) & 0xff); + **begin = byte; + } + } + return true; + } +}; + +template +inline std::optional read_le(fibre::cbufptr_t* buffer) { + static_assert(is_complete>(), "no LittleEndianSerializer is defined for type T"); + return LittleEndianSerializer::read(&buffer->begin(), buffer->end()); +} + +template +inline bool write_le(T value, fibre::bufptr_t* buffer) { + static_assert(is_complete>(), "no LittleEndianSerializer is defined for type T"); + return LittleEndianSerializer::write(value, &buffer->begin(), buffer->end()); +} + + +#endif \ No newline at end of file diff --git a/Firmware/fibre/cpp/interfaces_template.j2 b/Firmware/fibre/cpp/interfaces_template.j2 new file mode 100644 index 00000000..f4eb3163 --- /dev/null +++ b/Firmware/fibre/cpp/interfaces_template.j2 @@ -0,0 +1,94 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains base classes that correspond to the interfaces defined in + * your interface file. The objects you publish should inherit from these + * interfaces. + * + */ + +#pragma GCC push_options +#pragma GCC optimize ("s") + +[%- macro rettype(func) %] +[%- if not func.out -%] +void +[%- elif func.out | length == 1 -%] +[[(func.out.values() | first).type.c_name]] +[%- else -%] +[% for arg in func.out.values() %][[arg.type]][[', ' if not loop.last]][% endfor %] +[%- endif -%] +[%- endmacro %] + +[%- macro render_interface(intf) %] +class [[intf.name | to_pascal_case]]Intf { +public: +[%- for intf in intf.interfaces -%] +[[render_interface(intf) | indent(4)]] +[%- endfor %] +[%- for enum in intf.enums %] + enum [[enum.name | to_pascal_case]] { +[%- for k, value in enum['values'].items() %] + [[((enum.name + k) | to_macro_case).ljust(32)]] = [% if enum.is_flags %]0x[['%08x' | format(value.value)]][% else %][[value.value]][% endif %], +[%- endfor %] + }; +[%- endfor %] + +[%- for property in intf.attributes.values() %] +[%- if property.type.fullname.startswith("fibre.Property") %] +[%- if not property.c_getter and not property.c_setter %] + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{&obj->[[property.c_name]]}; } + template static inline void get_[[property.name]](T* obj, void* ptr) { new (ptr) [[property.type.c_name]]{&obj->[[property.c_name]]}; }[# these are for the set_endpoint_from_float function. This is unmaintainable and should go away #] +[%- elif not property.c_setter %] + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }}; } + template static inline void get_[[property.name]](T* obj, void* ptr) { new (ptr) [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }}; } +[%- else %] + template static inline auto get_[[property.name]](T* obj) { return [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_name]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } + template static inline void get_[[property.name]](T* obj, void* ptr) { new (ptr) [[property.type.c_name]]{obj, [](void* ctx){ return ([[property.type.value_type.c_name]])((T*)ctx)->[[property.c_getter]]; }, [](void* ctx, [[property.type.value_type.c_name]] value){ ((T*)ctx)->[[property.c_setter]](value); }}; } +[%- endif %] +[%- else %] + template static inline auto get_[[property.name]](T* obj) { return &obj->[[property.c_name]]; } +[%- endif %] +[%- endfor %] + +[%- for func in intf.functions.values() %] + virtual [[rettype(func)]] [[func.name | to_snake_case]]([% for in in func.in.values() %][% if loop.index0 %][[in.type.c_name]] [[in.name]][[', ' if not loop.last]][% endif %][% endfor %]) = 0; +[%- endfor %] +[%- for func in intf.functions.values() %] +[%- for k, arg in func.in.items() | skip_first %] + [[arg.type.c_name]] [[func.name | to_snake_case]]_in_[[arg.name]]_; // for internal use by Fibre + template static auto get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj) { return Property<[[arg.type.c_name]]>{&obj->[[func.name | to_snake_case]]_in_[[arg.name]]_}; } + template static void get_[[func.name | to_snake_case]]_in_[[arg.name]]_(T* obj, void* ptr) { new (ptr) Property<[[arg.type.c_name]]>{&obj->[[func.name | to_snake_case]]_in_[[arg.name]]_}; } +[%- endfor %] +[%- for k, arg in func.out.items() %] + [[arg.type.c_name]] [[func.name | to_snake_case]]_out_[[arg.name]]_; // for internal use by Fibre + template static auto get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj) { return Property{&obj->[[func.name | to_snake_case]]_out_[[arg.name]]_}; } + template static void get_[[func.name | to_snake_case]]_out_[[arg.name]]_(T* obj, void* ptr) { new (ptr) Property{&obj->[[func.name | to_snake_case]]_out_[[arg.name]]_}; } +[%- endfor %] +[%- endfor %] +}; +[%- endmacro %] + +[% for intf in toplevel_interfaces %] +[[render_interface(intf)]] +[% endfor %] + +[%- for _, enum in value_types.items() %] +[%- if enum.is_flags %] +// this is technically not thread-safe but practically it might be +inline [[enum.c_name]] operator | ([[enum.c_name]] a, [[enum.c_name]] b) { return static_cast<[[enum.c_name]]>(static_cast>(a) | static_cast>(b)); } +inline [[enum.c_name]] operator & ([[enum.c_name]] a, [[enum.c_name]] b) { return static_cast<[[enum.c_name]]>(static_cast>(a) & static_cast>(b)); } +inline [[enum.c_name]] operator ^ ([[enum.c_name]] a, [[enum.c_name]] b) { return static_cast<[[enum.c_name]]>(static_cast>(a) ^ static_cast>(b)); } +inline [[enum.c_name]]& operator |= ([[enum.c_name]] &a, [[enum.c_name]] b) { return reinterpret_cast<[[enum.c_name]]&>(reinterpret_cast&>(a) |= static_cast>(b)); } +inline [[enum.c_name]]& operator &= ([[enum.c_name]] &a, [[enum.c_name]] b) { return reinterpret_cast<[[enum.c_name]]&>(reinterpret_cast&>(a) &= static_cast>(b)); } +inline [[enum.c_name]]& operator ^= ([[enum.c_name]] &a, [[enum.c_name]] b) { return reinterpret_cast<[[enum.c_name]]&>(reinterpret_cast&>(a) ^= static_cast>(b)); } +inline [[enum.c_name]] operator ~ ([[enum.c_name]] a) { return static_cast<[[enum.c_name]]>(~static_cast>(a)); } +[%- endif %] +[%- endfor %] + + + +#pragma GCC pop_options diff --git a/Firmware/fibre/cpp/protocol.cpp b/Firmware/fibre/cpp/protocol.cpp index 23a2e0a5..e8285c87 100644 --- a/Firmware/fibre/cpp/protocol.cpp +++ b/Firmware/fibre/cpp/protocol.cpp @@ -13,18 +13,11 @@ /* Global constant data ------------------------------------------------------*/ /* Global variables ----------------------------------------------------------*/ -Endpoint** endpoint_list_ = nullptr; // initialized by calling fibre_publish -size_t n_endpoints_ = 0; // initialized by calling fibre_publish -uint16_t json_crc_; // initialized by calling fibre_publish -JSONDescriptorEndpoint json_file_endpoint_ = JSONDescriptorEndpoint(); -EndpointProvider* application_endpoints_; - /* Private constant data -----------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/ static void hexdump(const uint8_t* buf, size_t len); -static inline int write_string(const char* str, StreamSink* output); /* Function implementations --------------------------------------------------*/ @@ -115,40 +108,27 @@ int StreamBasedPacketSink::process_packet(const uint8_t *buffer, size_t length) } - -void JSONDescriptorEndpoint::write_json(size_t id, StreamSink* output) { - write_string("{\"name\":\"\",", output); - - // write endpoint ID - write_string("\"id\":", output); - char id_buf[10]; - snprintf(id_buf, sizeof(id_buf), "%u", (unsigned)id); // TODO: get rid of printf - write_string(id_buf, output); - - write_string(",\"type\":\"json\",\"access\":\"r\"}", output); -} - -void JSONDescriptorEndpoint::register_endpoints(Endpoint** list, size_t id, size_t length) { - if (id < length) - list[id] = this; -} - // Returns part of the JSON interface definition. -void JSONDescriptorEndpoint::handle(const uint8_t* input, size_t input_length, StreamSink* output) { +bool fibre::endpoint0_handler(fibre::cbufptr_t* input_buffer, fibre::bufptr_t* output_buffer) { // The request must contain a 32 bit integer to specify an offset - if (input_length < 4) - return; - uint32_t offset = 0; - read_le(&offset, input); - NullStreamSink output_with_offset = NullStreamSink(offset, *output); - - size_t id = 0; - write_string("[", &output_with_offset); - json_file_endpoint_.write_json(id, &output_with_offset); - id += decltype(json_file_endpoint_)::endpoint_count; - write_string(",", &output_with_offset); - application_endpoints_->write_json(id, &output_with_offset); - write_string("]", &output_with_offset); + std::optional offset = read_le(input_buffer); + + if (!offset.has_value()) { + // Didn't receive any offset + return false; + } else if (offset.value() == 0xffffffff) { + // If the offset is special value 0xFFFFFFFF, send back the JSON version ID instead + return write_le(json_version_id_, output_buffer); + } else if (offset.value() >= embedded_json_length) { + // Attempt to read beyond the buffer end - return empty response + return true; + } else { + // Return part of the json file + size_t n_copy = std::min(output_buffer->size(), embedded_json_length - (size_t)offset.value()); + memcpy(output_buffer->begin(), embedded_json + offset.value(), n_copy); + *output_buffer = output_buffer->skip(n_copy); + return true; + } } int BidirectionalPacketBasedChannel::process_packet(const uint8_t* buffer, size_t length) { @@ -169,19 +149,10 @@ int BidirectionalPacketBasedChannel::process_packet(const uint8_t* buffer, size_ bool expect_response = endpoint_id & 0x8000; endpoint_id &= 0x7fff; - if (endpoint_id >= n_endpoints_) - return -1; - - Endpoint* endpoint = endpoint_list_[endpoint_id]; - if (!endpoint) { - LOG_FIBRE("critical: no endpoint at %d", endpoint_id); - return -1; - } - // Verify packet trailer. The expected trailer value depends on the selected endpoint. // For endpoint 0 this is just the protocol version, for all other endpoints it's a // CRC over the entire JSON descriptor tree (this may change in future versions). - uint16_t expected_trailer = endpoint_id ? json_crc_ : PROTOCOL_VERSION; + uint16_t expected_trailer = endpoint_id ? fibre::json_crc_ : PROTOCOL_VERSION; uint16_t actual_trailer = buffer[length - 2] | (buffer[length - 1] << 8); if (expected_trailer != actual_trailer) { LOG_FIBRE("trailer mismatch for endpoint %d: expected %04x, got %04x\r\n", endpoint_id, expected_trailer, actual_trailer); @@ -197,12 +168,13 @@ int BidirectionalPacketBasedChannel::process_packet(const uint8_t* buffer, size_ if (expected_response_length > sizeof(tx_buf_) - 2) expected_response_length = sizeof(tx_buf_) - 2; - MemoryStreamSink output(tx_buf_ + 2, expected_response_length); - endpoint->handle(buffer, length - 2, &output); + fibre::cbufptr_t input_buffer{buffer, length - 2}; + fibre::bufptr_t output_buffer{tx_buf_ + 2, expected_response_length}; + fibre::endpoint_handler(endpoint_id, &input_buffer, &output_buffer); // Send response if (expect_response) { - size_t actual_response_length = expected_response_length - output.get_free_space() + 2; + size_t actual_response_length = expected_response_length - output_buffer.size() + 2; write_le(seq_no | 0x8000, tx_buf_); LOG_FIBRE("send packet:\r\n"); @@ -213,15 +185,3 @@ int BidirectionalPacketBasedChannel::process_packet(const uint8_t* buffer, size_ return 0; } - -bool is_endpoint_ref_valid(endpoint_ref_t endpoint_ref) { - return (endpoint_ref.json_crc == json_crc_) - && (endpoint_ref.endpoint_id < n_endpoints_); -} - -Endpoint* get_endpoint(endpoint_ref_t endpoint_ref) { - if (is_endpoint_ref_valid(endpoint_ref)) - return endpoint_list_[endpoint_ref.endpoint_id]; - else - return nullptr; -} diff --git a/Firmware/fibre/cpp/type_info_template.j2 b/Firmware/fibre/cpp/type_info_template.j2 new file mode 100644 index 00000000..7e6cda57 --- /dev/null +++ b/Firmware/fibre/cpp/type_info_template.j2 @@ -0,0 +1,50 @@ +/*[# This is the original template, thus the warning below does not apply to this file #] + * ============================ WARNING ============================ + * ==== This is an autogenerated file. ==== + * ==== Any changes to this file will be lost when recompiling. ==== + * ================================================================= + * + * This file contains support functions for the ODrive ASCII protocol. + * + * TODO: might generalize this as an approach to runtime introspection. + */ + +#include + +#pragma GCC push_options +#pragma GCC optimize ("s") + +[% for intf in interfaces.values() %][% if not intf.builtin %] +template +struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo { + using TypeInfo::TypeInfo; + static const PropertyInfo property_table[]; + static const [[intf.fullname | to_pascal_case]]TypeInfo singleton; + static Introspectable make_introspectable(T& obj) { return TypeInfo::make_introspectable(&obj, &singleton); } + + introspectable_storage_t get_child(introspectable_storage_t obj, size_t idx) const override { + T* ptr = *(T**)&obj; + introspectable_storage_t res; + switch (idx) { +[%- for property in intf.attributes.values() %] + case [[loop.index0]]: *(decltype([[intf.c_name]]::get_[[property.name]](std::declval()))*)(&res) = [[intf.c_name]]::get_[[property.name]](ptr); break; +[%- endfor %] + } + return res; + } +}; +[% endif %][% endfor %] + +[% for intf in interfaces.values() %][% if not intf.builtin %] +template +const PropertyInfo [[intf.fullname | to_pascal_case]]TypeInfo::property_table[] = { +[%- for property in intf.attributes.values() %] + {"[[property.name]]", &[[(property.type.purename or property.type.fullname) | to_pascal_case]]TypeInfo()))>>::singleton}, +[%- endfor %] +}; +template +const [[intf.fullname | to_pascal_case]]TypeInfo [[intf.fullname | to_pascal_case]]TypeInfo::singleton{[[intf.fullname | to_pascal_case]]TypeInfo::property_table, sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table) / sizeof([[intf.fullname | to_pascal_case]]TypeInfo::property_table[0])}; + +[% endif %][% endfor %] + +#pragma GCC pop_options diff --git a/Firmware/fibre/python/fibre/discovery.py b/Firmware/fibre/python/fibre/discovery.py index a3751633..bd4ce071 100644 --- a/Firmware/fibre/python/fibre/discovery.py +++ b/Firmware/fibre/python/fibre/discovery.py @@ -7,11 +7,14 @@ import json import time import threading import traceback +import struct import fibre.protocol import fibre.utils import fibre.remote_object from fibre.utils import Event, Logger from fibre.protocol import ChannelBrokenException, TimeoutError +import appdirs +import os # Load all installed transport layers @@ -64,25 +67,57 @@ def find_all(path, serial_number, """ try: logger.debug("Connecting to device on " + channel._name) + + cache_dir = appdirs.user_cache_dir("odrivetool") + cache_path = None + + # Fetch the json version tag to check cache (only supported on firmware v0.5 or later) try: + json_version_tag = channel.remote_endpoint_operation(0, struct.pack("> 8) & 0xff)) - try: + try: + json_string = json_bytes.decode("ascii") + except UnicodeDecodeError: + logger.debug("Device responded on endpoint 0 with something that is not ASCII") + raise UnicodeDecodeError + + json_crc16 = fibre.protocol.calc_crc16(fibre.protocol.PROTOCOL_VERSION, json_bytes) json_data = json.loads(json_string) - except json.decoder.JSONDecodeError as error: - logger.debug("device responded on endpoint 0 with something that is not JSON: " + str(error)) - return + + # Save JSON to cache + if not cache_path is None: + logger.debug("Creating new JSON cache file {}".format(cache_path)) + os.makedirs(cache_dir, exist_ok=True) + with open(cache_path, 'w+') as json_cache: + json_cache.write(json_string) + logger.debug("Saved JSON to cache file {}".format(cache_path)) + + channel._interface_definition_crc = json_crc16 + + logger.debug("JSON: " + str(json_data).replace("{'name'", "\n{'name'")) + json_data = {"name": "fibre_node", "members": json_data} obj = fibre.remote_object.RemoteObject(json_data, None, channel, logger) @@ -93,7 +128,10 @@ def find_all(path, serial_number, if serial_number != None and device_serial_number != serial_number: logger.debug("Ignoring device with serial number {}".format(device_serial_number)) return + did_discover_object_callback(obj) + + except Exception: logger.debug("Unexpected exception after discovering channel: " + traceback.format_exc()) @@ -112,18 +150,30 @@ def find_all(path, serial_number, def find_any(path="usb", serial_number=None, search_cancellation_token=None, channel_termination_token=None, - timeout=None, logger=Logger(verbose=False)): + timeout=None, logger=Logger(verbose=False), find_multiple=False): """ Blocks until the first matching Fibre node is connected and then returns that node """ - result = [ None ] + result = [] done_signal = Event(search_cancellation_token) def did_discover_object(obj): - result[0] = obj - done_signal.set() + result.append(obj) + if find_multiple: + if len(result) >= int(find_multiple): + done_signal.set() + else: + done_signal.set() + find_all(path, serial_number, did_discover_object, done_signal, channel_termination_token, logger) try: done_signal.wait(timeout=timeout) + except TimeoutError: + if not find_multiple: + return None finally: done_signal.set() # terminate find_all - return result[0] + + if find_multiple: + return result + else: + return result[0] diff --git a/Firmware/fibre/python/fibre/protocol.py b/Firmware/fibre/python/fibre/protocol.py index 851ad457..c9d65735 100644 --- a/Firmware/fibre/python/fibre/protocol.py +++ b/Firmware/fibre/python/fibre/protocol.py @@ -28,6 +28,8 @@ CRC16_DEFAULT = 0x3d65 # this must match the polynomial in the C++ implementatio MAX_PACKET_SIZE = 128 +# For more information on the CRC algorithm refer to protocol.md + def calc_crc(remainder, value, polynomial, bitwidth): topbit = (1 << (bitwidth - 1)) @@ -61,9 +63,6 @@ def calc_crc16(remainder, value): remainder = calc_crc(remainder, value, CRC16_DEFAULT, 16) return remainder -# Can be verified with http://www.sunshine2k.de/coding/javascript/crc/crc_js.html: -#print(hex(calc_crc8(0x12, [1, 2, 3, 4, 5, 0x10, 0x13, 0x37]))) -#print(hex(calc_crc16(0xfeef, [1, 2, 3, 4, 5, 0x10, 0x13, 0x37]))) class DeviceInitException(Exception): pass diff --git a/Firmware/fibre/python/fibre/serial_transport.py b/Firmware/fibre/python/fibre/serial_transport.py index 931a9415..e737dfca 100644 --- a/Firmware/fibre/python/fibre/serial_transport.py +++ b/Firmware/fibre/python/fibre/serial_transport.py @@ -17,7 +17,8 @@ DEFAULT_BAUDRATE = 115200 class SerialStreamTransport(fibre.protocol.StreamSource, fibre.protocol.StreamSink): def __init__(self, port, baud): - self._dev = serial.Serial(port, baud, timeout=1) + self._timeout = 1 + self._dev = serial.Serial(port, baud, timeout=self._timeout) def process_bytes(self, bytes): self._dev.write(bytes) @@ -29,10 +30,16 @@ class SerialStreamTransport(fibre.protocol.StreamSource, fibre.protocol.StreamSi function blocks forever. A deadline before the current time corresponds to non-blocking mode. """ - if deadline is None: + # Only set new timeout value if it is reasonably different from the old one (e.g. 20% as below) + # Otherwise it adds significant overhead (at least under Win10) as the port is reset with every reconfiguration + if deadline is None and self._timeout is not None: + self._timeout = None self._dev.timeout = None - else: - self._dev.timeout = max(deadline - time.monotonic(), 0) + elif deadline is not None: + new_timeout = max(deadline - time.monotonic(), 0) + if abs(new_timeout - self._timeout) > self._timeout * 0.2: + self._timeout = new_timeout + self._dev.timeout = new_timeout return self._dev.read(n_bytes) def get_bytes_or_fail(self, n_bytes, deadline): diff --git a/Firmware/fibre/python/fibre/shell.py b/Firmware/fibre/python/fibre/shell.py index d5e24d85..c5a7257a 100644 --- a/Firmware/fibre/python/fibre/shell.py +++ b/Firmware/fibre/python/fibre/shell.py @@ -80,11 +80,23 @@ def launch_shell(args, # If IPython is installed, embed IPython shell, otherwise embed regular shell if use_ipython: - help = lambda: print_help(args, len(discovered_devices) > 0) # Override help function # pylint: disable=W0612 - locals()['__name__'] = globals()['__name__'] # to fix broken "%run -i script.py" + # Override help function # pylint: disable=W0612 + help = lambda: print_help(args, len(discovered_devices) > 0) + # to fix broken "%run -i script.py" + locals()['__name__'] = globals()['__name__'] console = IPython.terminal.embed.InteractiveShellEmbed(banner1='') - console.runcode = console.run_code # hack to make IPython look like the regular console + + # hack to make IPython look like the regular console + console.runcode = console.run_cell interact = console + + # Catch ChannelBrokenException (since disconnect is not always an error) + default_exception_hook = console._showtraceback + def filtered_exception_hook(ex_class, ex, trace): + if(ex_class.__module__+'.'+ex_class.__name__ != 'fibre.protocol.ChannelBrokenException'): + default_exception_hook(ex_class,ex,trace) + + console._showtraceback = filtered_exception_hook else: # Enable tab complete if possible try: @@ -100,13 +112,13 @@ def launch_shell(args, console = code.InteractiveConsole(locals=interactive_variables) interact = lambda: console.interact(banner='') - # install hook to hide ChannelBrokenException - console.runcode('import sys') - console.runcode('superexcepthook = sys.excepthook') - console.runcode('def newexcepthook(ex_class,ex,trace):\n' - ' if ex_class.__module__ + "." + ex_class.__name__ != "fibre.ChannelBrokenException":\n' - ' superexcepthook(ex_class,ex,trace)') - console.runcode('sys.excepthook=newexcepthook') + # Catch ChannelBrokenException (since disconnect is not alway an error) + console.runcode("import sys") + console.runcode("default_exception_hook = sys.excepthook") + console.runcode("def filtered_exception_hook(ex_class, ex, trace):\n" + " if ex_class.__module__ + '.' + ex_class.__name__ != 'fibre.protocol.ChannelBrokenException':\n" + " default_exception_hook(ex_class,ex,trace)") + console.runcode("sys.excepthook=filtered_exception_hook") # Launch shell diff --git a/Firmware/fibre/python/fibre/usbbulk_transport.py b/Firmware/fibre/python/fibre/usbbulk_transport.py index dd32b106..6643c8b2 100644 --- a/Firmware/fibre/python/fibre/usbbulk_transport.py +++ b/Firmware/fibre/python/fibre/usbbulk_transport.py @@ -187,7 +187,7 @@ def discover_channels(path, serial_number, callback, cancellation_token, channel return True while not cancellation_token.is_set(): - logger.debug("USB discover loop") + # logger.debug("USB discover loop") devices = usb.core.find(find_all=True, custom_match=device_matcher) for usb_device in devices: try: @@ -200,14 +200,15 @@ def discover_channels(path, serial_number, callback, cancellation_token, channel channel.usb_device = usb_device # for debugging only except usb.core.USBError as ex: if ex.errno == 13: - logger.debug("USB device access denied. Did you set up your udev rules correctly?") - continue + # TODO: this is an ODrive specific message and should live outside of the fibre library + logger.warn("I found a USB device that looks like an ODrive (bus {}, device {}) but I can't access it. Try running `sudo odrivetool udev-setup`, then unplug and replug the device.".format(usb_device.bus, usb_device.address)) + known_devices.append((usb_device.bus, usb_device.address)) elif ex.errno == 16: logger.debug("USB device busy. I'll reset it and try again.") usb_device.reset() continue else: - logger.debug("USB device init failed. Ignoring this device. More info: " + traceback.format_exc()) + logger.warn("USB device init failed (bus {}, device {}). Ignoring this device. More info: ".format(usb_device.bus, usb_device.address) + traceback.format_exc()) known_devices.append((usb_device.bus, usb_device.address)) else: known_devices.append((usb_device.bus, usb_device.address)) diff --git a/Firmware/fibre/python/setup.py b/Firmware/fibre/python/setup.py index 85f266f3..9edccf6b 100644 --- a/Firmware/fibre/python/setup.py +++ b/Firmware/fibre/python/setup.py @@ -76,7 +76,9 @@ setup( license='MIT', url = 'https://github.com/samuelsadok/fibre', keywords = ['communication', 'transport-layer', 'rpc'], - install_requires = [], + install_requires = [ + 'appdirs', # Used to find caching directory + ], #package_data={'': ['version.txt']}, classifiers = [], ) diff --git a/Firmware/fibre/tools/interface_generator.py b/Firmware/fibre/tools/interface_generator.py new file mode 100644 index 00000000..659d97f6 --- /dev/null +++ b/Firmware/fibre/tools/interface_generator.py @@ -0,0 +1,685 @@ +#!/bin/python3 + +import yaml +import json +import jinja2 +import jsonschema +import re +import argparse +import sys +from collections import OrderedDict + +# This schema describes what we expect interface definition files to look like +validator = jsonschema.Draft4Validator(yaml.safe_load(""" +definitions: + interface: + type: object + properties: + c_is_class: {type: boolean} + c_name: {type: string} + brief: {type: string} + doc: {type: string} + functions: + type: object + additionalProperties: {"$ref": "#/definitions/function"} + attributes: + type: object + additionalProperties: {"$ref": "#/definitions/attribute"} + __line__: {type: object} + __column__: {type: object} + required: [c_is_class] + additionalProperties: false + + valuetype: + type: object + properties: + mode: {type: string} # this shouldn't be here + c_name: {type: string} + values: {type: object} + flags: {type: object} + nullflag: {type: string} + __line__: {type: object} + __column__: {type: object} + additionalProperties: false + + intf_or_val_type: + anyOf: + - {"$ref": "#/definitions/interface"} + - {"$ref": "#/definitions/valuetype"} + - {"type": "string"} + + attribute: + anyOf: # this is probably not being used correctly + - {"$ref": "#/definitions/intf_or_val_type"} + - type: object + - type: object + properties: + type: {"$ref": "#/definitions/intf_or_val_type"} + c_name: {"type": string} + unit: {"type": string} + doc: {"type": string} + additionalProperties: false + + function: + anyOf: + - type: 'null' + - type: object + properties: + in: {type: object} + out: {type: object} + brief: {type: string} + doc: {type: string} + __line__: {type: object} + __column__: {type: object} + additionalProperties: false + + +type: object +properties: + ns: {type: string} + version: {type: string} + summary: {type: string} + dictionary: {type: array, items: {type: string}} + interfaces: + type: object + additionalProperties: { "$ref": "#/definitions/interface" } + valuetypes: + type: object + additionalProperties: { "$ref": "#/definitions/valuetype" } + __line__: {type: object} + __column__: {type: object} +additionalProperties: false +""")) + +# Source: https://stackoverflow.com/a/53647080/3621512 +class SafeLineLoader(yaml.SafeLoader): + pass +# def compose_node(self, parent, index): +# # the line number where the previous token has ended (plus empty lines) +# line = self.line +# node = super(SafeLineLoader, self).compose_node(parent, index) +# node.__line__ = line + 1 +# return node +# +# def construct_mapping(self, node, deep=False): +# mapping = super(SafeLineLoader, self).construct_mapping(node, deep=deep) +# mapping['__line__'] = node.__line__ +# #mapping['__column__'] = node.start_mark.column + 1 +# return mapping + +# Ensure that dicts remain ordered, even in Python <3.6 +# source: https://stackoverflow.com/a/21912744/3621512 +def construct_mapping(loader, node): + loader.flatten_mapping(node) + return OrderedDict(loader.construct_pairs(node)) +SafeLineLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping) + +dictionary = [] + +def get_words(string): + """ + Splits a string in PascalCase into a list of lower case words + """ + regex = ''.join((re.escape(w) + '|') for w in dictionary) + '[a-z0-9]+|[A-Z][a-z0-9]*' + return [(w if w in dictionary else w.lower()) for w in re.findall(regex, string)] + +def join_name(*names, delimiter: str = '.'): + """ + Joins two name components. + e.g. 'io.helloworld' + 'sayhello' => 'io.helloworld.sayhello' + """ + return delimiter.join(y for x in names for y in x.split(delimiter) if y != '') + +def split_name(name, delimiter: str = '.'): + def replace_delimiter_in_parentheses(): + parenthesis_depth = 0 + for c in name: + parenthesis_depth += 1 if c == '<' else -1 if c == '>' else 0 + yield c if (parenthesis_depth == 0) or (c != delimiter) else ':' + return [part.replace(':', '.') for part in ''.join(replace_delimiter_in_parentheses()).split('.')] + +def to_pascal_case(s): return ''.join([(w.title() if not w in dictionary else w) for w in get_words(s)]) +def to_camel_case(s): return ''.join([(c.lower() if i == 0 else c) for i, c in enumerate(''.join([w.title() for w in get_words(s)]))]) +def to_macro_case(s): return '_'.join(get_words(s)).upper() +def to_snake_case(s): return '_'.join(get_words(s)).lower() +def to_kebab_case(s): return '-'.join(get_words(s)).lower() + +value_types = OrderedDict({ + 'bool': {'builtin': True, 'fullname': 'bool', 'name': 'bool', 'c_name': 'bool', 'py_type': 'bool'}, + 'float32': {'builtin': True, 'fullname': 'float32', 'name': 'float32', 'c_name': 'float', 'py_type': 'float'}, + 'uint8': {'builtin': True, 'fullname': 'uint8', 'name': 'uint8', 'c_name': 'uint8_t', 'py_type': 'int'}, + 'uint16': {'builtin': True, 'fullname': 'uint16', 'name': 'uint16', 'c_name': 'uint16_t', 'py_type': 'int'}, + 'uint32': {'builtin': True, 'fullname': 'uint32', 'name': 'uint32', 'c_name': 'uint32_t', 'py_type': 'int'}, + 'uint64': {'builtin': True, 'fullname': 'uint64', 'name': 'uint64', 'c_name': 'uint64_t', 'py_type': 'int'}, + 'int8': {'builtin': True, 'fullname': 'int8', 'name': 'int8', 'c_name': 'int8_t', 'py_type': 'int'}, + 'int16': {'builtin': True, 'fullname': 'int16', 'name': 'int16', 'c_name': 'int16_t', 'py_type': 'int'}, + 'int32': {'builtin': True, 'fullname': 'int32', 'name': 'int32', 'c_name': 'int32_t', 'py_type': 'int'}, + 'int64': {'builtin': True, 'fullname': 'int64', 'name': 'int64', 'c_name': 'int64_t', 'py_type': 'int'}, + 'endpoint_ref': {'builtin': True, 'fullname': 'endpoint_ref', 'name': 'endpoint_ref', 'c_name': 'endpoint_ref_t', 'py_type': '[not implemented]'}, +}) + +enums = OrderedDict() + +interfaces = OrderedDict() + +def make_property_type(typeargs): + value_type = resolve_valuetype('', typeargs['fibre.Property.type']) + mode = typeargs.get('fibre.Property.mode', 'readwrite') + name = 'Property<' + value_type['fullname'] + ', ' + mode + '>' + fullname = join_name('fibre', name) + if fullname in interfaces: + return interfaces[fullname] + + c_name = 'Property<' + ('const ' if mode == 'readonly' else '') + value_type['c_name'] + '>' + prop_type = { + 'name': name, + 'fullname': fullname, + 'purename': 'fibre.Property', + 'c_name': c_name, + 'value_type': value_type, # TODO: should be a metaarg + 'mode': mode, # TODO: should be a metaarg + 'builtin': True, + 'attributes': OrderedDict(), + 'functions': OrderedDict() + } + if mode != 'readonly': + prop_type['functions']['exchange'] = { + 'name': 'exchange', + 'fullname': join_name(fullname, 'exchange'), + 'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': c_name}}), ('value', {'name': 'value', 'type': value_type, 'optional': True})]), + 'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]), + #'implementation': 'fibre_property_exchange<' + value_type['c_name'] + '>' + } + else: + prop_type['functions']['read'] = { + 'name': 'read', + 'fullname': join_name(fullname, 'read'), + 'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': c_name}})]), + 'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]), + #'implementation': 'fibre_property_read<' + value_type['c_name'] + '>' + } + + interfaces[fullname] = prop_type + return prop_type + +generics = { + 'fibre.Property': make_property_type # TODO: improve generic support +} + + +def make_ref_type(interface): + name = 'Ref<' + interface['fullname'] + '>' + fullname = join_name('fibre', name) + if fullname in interfaces: + return interfaces[fullname] + + ref_type = { + 'builtin': True, + 'name': name, + 'fullname': fullname, + 'c_name': interface['fullname'].replace('.', 'Intf::') + 'Intf*' + } + value_types[fullname] = ref_type + + return ref_type + +def get_dict(elem, key): + return elem.get(key, None) or OrderedDict() + +def regularize_arg(path, name, elem): + if elem is None: + elem = {} + elif isinstance(elem, str): + elem = {'type': elem} + elem['name'] = name + elem['fullname'] = path = join_name(path, name) + elem['type'] = regularize_valuetype(path, name, elem['type']) + return elem + +def regularize_func(path, name, elem, prepend_args): + if elem is None: + elem = {} + elem['name'] = name + elem['fullname'] = path = join_name(path, name) + elem['in'] = OrderedDict((n, regularize_arg(path, n, arg)) + for n, arg in (*prepend_args.items(), *get_dict(elem, 'in').items())) + elem['out'] = OrderedDict((n, regularize_arg(path, n, arg)) + for n, arg in get_dict(elem, 'out').items()) + return elem + +def regularize_attribute(parent, name, elem, c_is_class): + if elem is None: + elem = {} + if isinstance(elem, str): + elem = {'type': elem} + elif not 'type' in elem: + elem['type'] = {} + if 'attributes' in elem: elem['type']['attributes'] = elem.pop('attributes') + if 'functions' in elem: elem['type']['functions'] = elem.pop('functions') + if 'c_is_class' in elem: elem['type']['c_is_class'] = elem.pop('c_is_class') + if 'values' in elem: elem['type']['values'] = elem.pop('values') + if 'flags' in elem: elem['type']['flags'] = elem.pop('flags') + if 'nullflag' in elem: elem['type']['nullflag'] = elem.pop('nullflag') + + elem['name'] = name + elem['fullname'] = join_name(parent['fullname'], name) + elem['parent'] = parent + elem['typeargs'] = elem.get('typeargs', {}) + elem['c_name'] = elem.get('c_name', None) or (elem['name'] + ('_' if c_is_class else '')) + if ('c_getter' in elem) or ('c_setter' in elem): + elem['c_getter'] = elem.get('c_getter', elem['c_name']) + elem['c_setter'] = elem.get('c_setter', elem['c_name'] + ' = ') + + if isinstance(elem['type'], str) and elem['type'].startswith('readonly '): + elem['typeargs']['fibre.Property.mode'] = 'readonly' + elem['typeargs']['fibre.Property.type'] = elem['type'][len('readonly '):] + elem['type'] = 'fibre.Property' + if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter') + elif ('flags' in elem['type']) or ('values' in elem['type']): + elem['typeargs']['fibre.Property.mode'] = elem['typeargs'].get('fibre.Property.mode', None) or 'readwrite' + elem['typeargs']['fibre.Property.type'] = regularize_valuetype(parent['fullname'], to_pascal_case(name), elem['type']) + elem['type'] = 'fibre.Property' + if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter') + else: + elem['type'] = regularize_interface(parent['fullname'], to_pascal_case(name), elem['type']) + return elem + + +def regularize_interface(path, name, elem): + if elem is None: + elem = {} + if isinstance(elem, str): + return elem # will be resolved during type resolution + #if path is None: + # max_anonymous_type = max([int((re.findall('^' + join_name(path, 'AnonymousType') + '([1-9]+)$', x) + ['0'])[0]) for x in interfaces.keys()]) + # path = 'AnonymousType' + str(max_anonymous_type + 1) + elem['name'] = split_name(name)[-1] + elem['fullname'] = path = join_name(path, name) + elem['c_name'] = elem.get('c_name', elem['fullname'].replace('.', 'Intf::')) + 'Intf' + interfaces[path] = elem + elem['functions'] = OrderedDict((name, regularize_func(path, name, func, {'obj': {'type': make_ref_type(elem)}})) + for name, func in get_dict(elem, 'functions').items()) + if not 'c_is_class' in elem: + raise Exception(elem) + treat_as_class = elem['c_is_class'] # TODO: add command line arg to make this selectively optional + elem['attributes'] = OrderedDict((name, regularize_attribute(elem, name, prop, treat_as_class)) + for name, prop in get_dict(elem, 'attributes').items()) + elem['interfaces'] = [] + elem['enums'] = [] + return elem + +def regularize_valuetype(path, name, elem): + if elem is None: + elem = {} + if isinstance(elem, str): + return elem # will be resolved during type resolution + elem['name'] = split_name(name)[-1] + elem['fullname'] = path = join_name(path, name) + elem['c_name'] = elem.get('c_name', elem['fullname'].replace('.', 'Intf::')) + value_types[path] = elem + + if 'flags' in elem: # treat as flags + bit = 0 + for k, v in elem['flags'].items(): + elem['flags'][k] = elem['flags'][k] or OrderedDict() + elem['flags'][k]['name'] = k + current_bit = elem['flags'][k].get('bit', bit) + elem['flags'][k]['bit'] = current_bit + elem['flags'][k]['value'] = 0 if current_bit is None else (1 << current_bit) + bit = bit if current_bit is None else current_bit + 1 + if 'nullflag' in elem: + elem['flags'] = OrderedDict([(elem['nullflag'], {'value': 0, 'bit': None}), *elem['flags'].items()]) + elem['values'] = elem['flags'] + elem['is_flags'] = True + elem['is_enum'] = True + enums[path] = elem + + elif 'values' in elem: # treat as enum + val = 0 + for k, v in elem['values'].items(): + elem['values'][k] = elem['values'][k] or OrderedDict() + elem['values'][k]['name'] = k + val = elem['values'][k].get('value', val) + elem['values'][k]['value'] = val + val += 1 + enums[path] = elem + elem['is_enum'] = True + + return elem + +def resolve_interface(scope, name, typeargs): + """ + Resolves a type name (i.e. interface name or value type name) given as a + string to an interface object. The innermost scope is searched first. + At every scope level, if no matching interface is found, it is checked if a + matching value type exists. If so, the interface type fibre.Property + is returned. + """ + if not isinstance(name, str): + return name + + if 'fibre.Property.type' in typeargs: + typeargs['fibre.Property.type'] = resolve_valuetype(scope, typeargs['fibre.Property.type']) + + scope = scope.split('.') + for probe_scope in [join_name(*scope[:(len(scope)-i)]) for i in range(len(scope)+1)]: + probe_name = join_name(probe_scope, name) + #print('probing ' + probe_name) + if probe_name in interfaces: + return interfaces[probe_name] + elif probe_name in value_types: + typeargs['fibre.Property.type'] = value_types[probe_name] + return make_property_type(typeargs) + elif probe_name in generics: + return generics[probe_name](typeargs) + + raise Exception('could not resolve type {} in {}. Known interfaces are: {}. Known value types are: {}'.format(name, join_name(*scope), list(interfaces.keys()), list(value_types.keys()))) + +def resolve_valuetype(scope, name): + """ + Resolves a type name given as a string to the type object. + The innermost scope is searched first. + """ + if not isinstance(name, str): + return name + + scope = scope.split('.') + for probe_scope in [join_name(*scope[:(len(scope)-i)]) for i in range(len(scope)+1)]: + probe_name = join_name(probe_scope, name) + if probe_name in value_types: + return value_types[probe_name] + + raise Exception('could not resolve type {} in {}. Known value types are: {}'.format(name, join_name(*scope), list(value_types.keys()))) + + +def map_to_fibre01_type(t): + if t.get('is_enum', False): + return 'int32' + elif t['fullname'] == 'float32': + return 'float' + return t['fullname'] + +def generate_endpoint_for_property(prop, attr_bindto, idx): + prop_intf = interfaces[prop['type']['fullname']] + + endpoint = { + 'id': idx, + 'function': prop_intf['functions']['read' if prop['type']['mode'] == 'readonly' else 'exchange'], + 'in_bindings': OrderedDict([('obj', attr_bindto)]), + 'out_bindings': OrderedDict() + } + endpoint_definition = { + 'name': prop['name'], + 'id': idx, + 'type': map_to_fibre01_type(prop['type']['value_type']), + 'access': 'r' if prop['type']['mode'] == 'readonly' else 'rw', + } + return endpoint, endpoint_definition + +def generate_endpoint_table(intf, bindto, idx): + """ + Generates a Fibre v0.1 endpoint table for a given interface. + This will probably be deprecated in the future. + The object must have no circular property types (i.e. A.b has type B and B.a has type A). + """ + endpoints = [] + endpoint_definitions = [] + cnt = 0 + + for k, prop in intf['attributes'].items(): + property_value_type = re.findall('^fibre\.Property<([^>]*), (readonly|readwrite)>$', prop['type']['fullname']) + #attr_bindto = join_name(bindto, bindings_map.get(join_name(intf['fullname'], k), k + ('_' if len(intf['functions']) or (intf['fullname'] in treat_as_classes) else ''))) + attr_bindto = intf['c_name'] + '::get_' + prop['name'] + '(' + bindto + ')' + if len(property_value_type): + # Special handling for Property<...> attributes: they resolve to one single endpoint + endpoint, endpoint_definition = generate_endpoint_for_property(prop, attr_bindto, idx + cnt) + endpoints.append(endpoint) + endpoint_definitions.append(endpoint_definition) + cnt += 1 + else: + inner_endpoints, inner_endpoint_definitions, inner_cnt = generate_endpoint_table(prop['type'], attr_bindto, idx + cnt) + endpoints += inner_endpoints + endpoint_definitions.append({ + 'name': k, + 'type': 'object', + 'members': inner_endpoint_definitions + }) + cnt += inner_cnt + + for k, func in intf['functions'].items(): + endpoints.append({ + 'id': idx + cnt, + 'function': func, + 'in_bindings': OrderedDict([('obj', bindto), *[(k_arg, '(' + bindto + ')->' + func['name'] + '_in_' + k_arg + '_') for k_arg in list(func['in'].keys())[1:]]]), + 'out_bindings': OrderedDict((k_arg, '&(' + bindto + ')->' + func['name'] + '_out_' + k_arg + '_') for k_arg in func['out'].keys()), + }) + in_def = [] + out_def = [] + for i, (k_arg, arg) in enumerate(list(func['in'].items())[1:]): + endpoint, endpoint_definition = generate_endpoint_for_property({ + 'name': arg['name'], + 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readwrite'}) + }, intf['c_name'] + '::get_' + func['name'] + '_in_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + 1 + i) + endpoints.append(endpoint) + in_def.append(endpoint_definition) + for i, (k_arg, arg) in enumerate(func['out'].items()): + endpoint, endpoint_definition = generate_endpoint_for_property({ + 'name': arg['name'], + 'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readonly'}) + }, intf['c_name'] + '::get_' + func['name'] + '_out_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + len(func['in']) + i) + endpoints.append(endpoint) + out_def.append(endpoint_definition) + + endpoint_definitions.append({ + 'name': k, + 'id': idx + cnt, + 'type': 'function', + 'inputs': in_def, + 'outputs': out_def + }) + cnt += len(func['in']) + len(func['out']) + + return endpoints, endpoint_definitions, cnt + + +# Parse arguments + +parser = argparse.ArgumentParser(description="Gernerate code from YAML interface definitions") +parser.add_argument("--version", action="store_true", + help="print version information") +parser.add_argument("-v", "--verbose", action="store_true", + help="print debug information (on stderr)") +parser.add_argument("-d", "--definitions", type=argparse.FileType('r', encoding='utf-8'), nargs='+', + help="the YAML interface definition file(s) used to generate the code") +parser.add_argument("-t", "--template", type=argparse.FileType('r', encoding='utf-8'), + help="the code template") +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("-o", "--output", type=argparse.FileType('w', encoding='utf-8'), + help="path of the generated output") +group.add_argument("--outputs", type=str, + help="path pattern for the generated outputs. One output is generated for each interface. Use # as placeholder for the interface name.") +parser.add_argument("--generate-endpoints", type=str, nargs='?', + help="if specified, an endpoint table will be generated and passed to the template for the specified interface") +args = parser.parse_args() + +if args.version: + print("0.0.1") + sys.exit(0) + + +definition_files = args.definitions +template_file = args.template + + +# Load definition files + +for definition_file in definition_files: + try: + file_content = yaml.load(definition_file, Loader=SafeLineLoader) + except yaml.scanner.ScannerError as ex: + print("YAML parsing error: " + str(ex), file=sys.stderr) + sys.exit(1) + for err in validator.iter_errors(file_content): + if '__line__' in err.absolute_path: + continue + if '__column__' in err.absolute_path: + continue + #instance = err.instance.get(re.findall("([^']*)' (?:was|were) unexpected\)", err.message)[0], err.instance) + # TODO: print line number + raise Exception(err.message + '\nat ' + str(list(err.absolute_path))) + interfaces.update(get_dict(file_content, 'interfaces')) + value_types.update(get_dict(file_content, 'valuetypes')) + dictionary += file_content.get('dictionary', None) or [] + + +# Preprocess definitions + +# Regularize everything into a wellknown form +for k, item in list(interfaces.items()): + regularize_interface('', k, item) +for k, item in list(value_types.items()): + regularize_valuetype('', k, item) + +if args.verbose: + print('Known interfaces: ' + ''.join([('\n ' + k) for k in interfaces.keys()])) + print('Known value types: ' + ''.join([('\n ' + k) for k in value_types.keys()])) + +clashing_names = list(set(value_types.keys()).intersection(set(interfaces.keys()))) +if len(clashing_names): + print("**Error**: Found both an interface and a value type with the name {}. This is not allowed, interfaces and value types (such as enums) share the same namespace.".format(clashing_names[0]), file=sys.stderr) + sys.exit(1) + +# Resolve all types into references +for _, item in list(interfaces.items()): + for _, prop in item['attributes'].items(): + prop['type'] = resolve_interface(item['fullname'], prop['type'], prop['typeargs']) + for _, func in item['functions'].items(): + for _, arg in func['in'].items(): + arg['type'] = resolve_valuetype(item['fullname'], arg['type']) + for _, arg in func['out'].items(): + arg['type'] = resolve_valuetype(item['fullname'], arg['type']) + +# Attach interfaces to their parents +toplevel_interfaces = [] +for k, item in list(interfaces.items()): + k = split_name(k) + if len(k) == 1: + toplevel_interfaces.append(item) + else: + if k[:-1] != ['fibre']: # TODO: remove special handling + parent = interfaces[join_name(*k[:-1])] + parent['interfaces'].append(item) + item['parent'] = parent +toplevel_enums = [] +for k, item in list(enums.items()): + k = split_name(k) + if len(k) == 1: + toplevel_enums.append(item) + else: + if k[:-1] != ['fibre']: # TODO: remove special handling + parent = interfaces[join_name(*k[:-1])] + parent['enums'].append(item) + item['parent'] = parent + + +if args.generate_endpoints: + endpoints, embedded_endpoint_definitions, _ = generate_endpoint_table(interfaces[args.generate_endpoints], '&ep_root', 1) # TODO: make user-configurable + embedded_endpoint_definitions = [{'name': '', 'id': 0, 'type': 'json', 'access': 'r'}] + embedded_endpoint_definitions + endpoints = [{'id': 0, 'function': {'fullname': 'endpoint0_handler', 'in': {}, 'out': {}}, 'bindings': {}}] + endpoints +else: + embedded_endpoint_definitions = None + endpoints = None + + +# Render template + +env = jinja2.Environment( + comment_start_string='[#', comment_end_string='#]', + block_start_string='[%', block_end_string='%]', + variable_start_string='[[', variable_end_string=']]' +) + +def tokenize(text, interface, interface_transform, value_type_transform, attribute_transform): + """ + Looks for referencable tokens (interface names, value type names or + attribute names) in a documentation text and runs them through the provided + processing functions. + Tokens are detected by enclosing back-ticks (`). + + interface: The interface type object that defines the scope in which the + tokens should be detected. + interface_transform: A function that takes an interface object as an argument + and returns a string. + value_type_transform: A function that takes a value type object as an argument + and returns a string. + attribute_transform: A function that takes the token strin and an attribute + object as arguments and returns a string. + """ + if text is None or isinstance(text, jinja2.runtime.Undefined): + return text + + def token_transform(token): + token = token.groups()[0] + token_list = split_name(token) + + # Check if this is an attribute reference + scope = interface + attr = None + while attr is None and not scope is None: + attr_intf = scope + for name in token_list: + if not name in attr_intf['attributes']: + attr = None + break + attr = attr_intf['attributes'][name] + attr_intf = attr['type'] + scope = scope.get('parent', None) + + if not attr is None: + return attribute_transform(token, attr) + + print('Warning: cannot resolve "{}" in {}'.format(token, interface['fullname'])) + return "`" + token + "`" + + return re.sub(r'`([A-Za-z\._]+)`', token_transform, text) + +env.filters['to_pascal_case'] = to_pascal_case +env.filters['to_camel_case'] = to_camel_case +env.filters['to_macro_case'] = to_macro_case +env.filters['to_snake_case'] = to_snake_case +env.filters['to_kebab_case'] = to_kebab_case +env.filters['first'] = lambda x: next(iter(x)) +env.filters['skip_first'] = lambda x: list(x)[1:] +env.filters['to_c_string'] = lambda x: '\n'.join(('"' + line.replace('"', '\\"') + '"') for line in json.dumps(x, separators=(',', ':')).replace('{"name"', '\n{"name"').split('\n')) +env.filters['tokenize'] = tokenize +env.filters['diagonalize'] = lambda lst: [lst[:i + 1] for i in range(len(lst))] + +template = env.from_string(template_file.read()) + +template_args = { + 'interfaces': interfaces, + 'value_types': value_types, + 'toplevel_interfaces': toplevel_interfaces, + 'endpoints': endpoints, + 'embedded_endpoint_definitions': embedded_endpoint_definitions +} + +if not args.output is None: + output = template.render(**template_args) + args.output.write(output) +else: + assert('#' in args.outputs) + + for k, intf in interfaces.items(): + if split_name(k)[0] == 'fibre': + continue # TODO: remove special case + output = template.render(interface = intf, **template_args) + with open(args.outputs.replace('#', k.lower()), 'w', encoding='utf-8') as output_file: + output_file.write(output) + + for k, enum in value_types.items(): + if enum.get('builtin', False) or not enum.get('is_enum', False): + continue + output = template.render(enum = enum, **template_args) + with open(args.outputs.replace('#', k.lower()), 'w', encoding='utf-8') as output_file: + output_file.write(output) diff --git a/Firmware/find_programmer.sh b/Firmware/find_programmer.sh index 4b184b6a..cea57038 100755 --- a/Firmware/find_programmer.sh +++ b/Firmware/find_programmer.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash openocd -d3 -f board/stm32f4discovery.cfg -c "hla_serial wrong_serial" 2>&1 | \ xxd -p | \ tr -d '\n' | \ diff --git a/Firmware/interface_generator_stub.py b/Firmware/interface_generator_stub.py new file mode 100644 index 00000000..d5b22093 --- /dev/null +++ b/Firmware/interface_generator_stub.py @@ -0,0 +1,12 @@ +#!/bin/python3 + +import sys +import os + +try: + exec(open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fibre', 'tools', 'interface_generator.py')).read()) +except ImportError as ex: + print(str(ex), file=sys.stderr) + print("Note that there are new compile-time dependencies since around v0.5.1.", file=sys.stderr) + print("Check out https://github.com/madcowswe/ODrive/blob/devel/docs/developer-guide.md#prerequisites for details.", file=sys.stderr) + exit(1) diff --git a/Firmware/odrive-interface.yaml b/Firmware/odrive-interface.yaml new file mode 100644 index 00000000..2c9060eb --- /dev/null +++ b/Firmware/odrive-interface.yaml @@ -0,0 +1,1040 @@ +--- +version: 0.0.1 +ns: com.odriverobotics +summary: ODrive Interface Definitions + +dictionary: [ODrive] # Prevent the word 'ODrive' from being detected as two words 'O' and 'Drive' + +interfaces: + ODrive: + c_is_class: True + brief: Toplevel interface of your ODrive. + doc: | + The odrv0, odrv1, ... objects that appear in odrivetool implement this + toplevel interface. + attributes: + vbus_voltage: + type: readonly float32 + unit: V + brief: Voltage on the DC bus as measured by the ODrive. + ibus: + type: readonly float32 + unit: A + brief: Current on the DC bus as calculated by the ODrive. + doc: | + A positive value means that the ODrive is consuming power from the power supply, + a negative value means that the ODrive is sourcing power to the power supply. + + This value is equal to the sum of the motor currents and the brake resistor currents. + The motor currents are measured, the brake resistor current is calculated based on + `config.brake_resistance`. + ibus_report_filter_k: + type: float32 + doc: | + Filter gain for the reported `ibus`. Set to a value below 1.0 to get a smoother + line when plotting `ibus`. Set to 1.0 to disable. This filter is only applied to + the reported value and not for internal calculations. + serial_number: readonly uint64 + hw_version_major: readonly uint8 + hw_version_minor: readonly uint8 + hw_version_variant: readonly uint8 + fw_version_major: readonly uint8 + fw_version_minor: readonly uint8 + fw_version_revision: readonly uint8 + fw_version_unreleased: + type: readonly uint8 + doc: 0 for official releases, 1 otherwise + brake_resistor_armed: readonly bool + brake_resistor_saturated: bool + system_stats: + c_is_class: False + attributes: + uptime: readonly uint32 + min_heap_space: readonly uint32 + min_stack_space_axis0: readonly uint32 + min_stack_space_axis1: readonly uint32 + min_stack_space_comms: readonly uint32 + min_stack_space_usb: readonly uint32 + min_stack_space_uart: readonly uint32 + min_stack_space_can: readonly uint32 + min_stack_space_usb_irq: readonly uint32 + min_stack_space_startup: readonly uint32 + stack_usage_axis0: readonly uint32 + stack_usage_axis1: readonly uint32 + stack_usage_comms: readonly uint32 + stack_usage_usb: readonly uint32 + stack_usage_uart: readonly uint32 + stack_usage_usb_irq: readonly uint32 + stack_usage_startup: readonly uint32 + stack_usage_can: readonly uint32 + usb: + c_is_class: False + attributes: + rx_cnt: readonly uint32 + tx_cnt: readonly uint32 + tx_overrun_cnt: readonly uint32 + i2c: + c_is_class: False + attributes: + addr: readonly uint8 + addr_match_cnt: readonly uint32 + rx_cnt: readonly uint32 + error_cnt: readonly uint32 + config: + c_is_class: False + attributes: + enable_uart: + type: bool + doc: 'TODO: changing this currently requires a reboot - fix this' + uart_baudrate: + type: uint32 + doc: | + Defines the baudrate used on the UART interface. + Some baudrates will have a small timing error due to hardware limitations. + + Here's an (incomplete) list of baudrates for ODrive v3.x: + + Configured | Actual | Error [%] + -------------|---------------|----------- + 1.2 KBps | 1.2 KBps | 0 + 2.4 KBps | 2.4 KBps | 0 + 9.6 KBps | 9.6 KBps | 0 + 19.2 KBps | 19.195 KBps | 0.02 + 38.4 KBps | 38.391 KBps | 0.02 + 57.6 KBps | 57.613 KBps | 0.02 + 115.2 KBps | 115.068 KBps | 0.11 + 230.4 KBps | 230.769 KBps | 0.16 + 460.8 KBps | 461.538 KBps | 0.16 + 921.6 KBps | 913.043 KBps | 0.93 + 1.792 MBps | 1.826 MBps | 1.9 + 1.8432 MBps | 1.826 MBps | 0.93 + + For more information refer to Section 30.3.4 and Table 142 (the column with f_PCLK = 42 MHz) in the + [STM datasheet](https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf). + enable_i2c_instead_of_can: + type: bool + doc: Changing this requires a reboot. + enable_ascii_protocol_on_usb: bool + max_regen_current: float32 + brake_resistance: + type: float32 + unit: Ohm + brief: Value of the brake resistor connected to the ODrive. + doc: Set to 0 to disable. + + dc_bus_undervoltage_trip_level: + type: float32 + unit: V + brief: Minimum voltage below which the motor stops operating. + dc_bus_overvoltage_trip_level: + type: float32 + unit: V + brief: Maximum voltage above which the motor stops operating. + doc: | + This protects against cases in which the power supply fails to dissipate + the brake power if the brake resistor is disabled. + The default is 26V for the 24V board version and 52V for the 48V board version. + + enable_dc_bus_overvoltage_ramp: + type: bool + status: experimental + brief: Enables the DC bus overvoltage ramp feature. + doc: | + If enabled, if the measured DC voltage exceeds `dc_bus_overvoltage_ramp_start`, + the ODrive will sink more power than usual into the the brake resistor + in an attempt to bring the voltage down again. + + The brake duty cycle is increased by the following amount: + + * `vbus_voltage` == `dc_bus_overvoltage_ramp_start` => brake_duty_cycle += 0% + * `vbus_voltage` == `dc_bus_overvoltage_ramp_end` => brake_duty_cycle += 100% + + Remarks: + - This feature is active even when all motors are disarmed. + - This feature is disabled if `brake_resistance` is non-positive. + dc_bus_overvoltage_ramp_start: + type: float32 + status: experimental + brief: See `enable_dc_bus_overvoltage_ramp`. + doc: Do not set this lower than your usual `vbus_voltage`, + unless you like fried brake resistors. + dc_bus_overvoltage_ramp_end: + type: float32 + status: experimental + brief: See `enable_dc_bus_overvoltage_ramp`. + doc: Must be larger than `dc_bus_overvoltage_ramp_start`, + otherwise the ramp feature is disabled. + + dc_max_positive_current: + type: float32 + unit: A + brief: Max current the power supply can source. + dc_max_negative_current: + type: float32 + unit: A + brief: Max current the power supply can sink. + doc: You most likely want a non-positive value here. Set to -INFINITY to disable. + + gpio1_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[0]'} # TODO: disable for ODrive v3.2 and older + gpio2_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[1]'} # TODO: disable for ODrive v3.2 and older + gpio3_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[2]'} # TODO: disable for ODrive v3.2 and older + gpio4_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[3]'} + gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[2]'} + gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]'} + user_config_loaded: readonly bool + + axis0: {type: Axis, c_name: get_axis(0)} + axis1: {type: Axis, c_name: get_axis(1)} + can: {type: Can, c_name: get_can()} + test_property: uint32 + + functions: + test_function: {in: {delta: int32}, out: {cnt: int32}} + get_oscilloscope_val: {in: {index: uint32}, out: {val: float32}} + get_adc_voltage: {in: {gpio: uint32}, out: {voltage: float32}} + save_configuration: + erase_configuration: + reboot: + enter_dfu_mode: + + ODrive.Can: + c_is_class: True + attributes: + error: + nullflag: None + flags: {DuplicateCanIds: } + config: + c_is_class: False + attributes: + baud_rate: readonly uint32 + protocol: Protocol + functions: + set_baud_rate: {in: {baudRate: uint32}} + + ODrive.Endpoint: + c_is_class: False + attributes: + endpoint: endpoint_ref + min: float32 + max: float32 + + ODrive.Axis: + c_is_class: True + attributes: + error: + nullflag: 'None' + flags: + InvalidState: + brief: An invalid state was requested. + doc: | + You tried to run a state before you are allowed to. Typically you + tried to run encoder calibration or closed loop control before the + motor was calibrated, or you tried to run closed loop control + before the encoder was calibrated. + DcBusUnderVoltage: + brief: The DC voltage fell below the limit configured in `config.dc_bus_undervoltage_trip_level`. + doc: | + Confirm that your power leads are connected securely. For initial + testing a 12V PSU which can supply a couple of amps should be + sufficient while the use of low current ‘wall wart’ plug packs may + lead to inconsistent behaviour and is not recommended. + + You can monitor your PSU voltage using liveplotter in odrivetool + by entering `start_liveplotter(lambda: [odrv0.vbus_voltage])`. If + you see your votlage drop below `config.dc_bus_undervoltage_trip_level` + (default: ~ 8V) then you will trip this error. Even a relatively + small motor can draw multiple kW momentary and so unless you have + a very large PSU or are running of a battery you may encounter + this error when executing high speed movements with a high current + limit. To limit your PSU power draw you can limit your motor + current and/or velocity limit `controller.config.vel_limit` and + `motor.config.current_lim`. + DcBusOverVoltage: + brief: The DC voltage exceeded the limit configured in `config.dc_bus_overvoltage_trip_level`. + doc: | + Confirm that you have a brake resistor of the correct value + connected securely and that `config.brake_resistance` is set to + the value of your brake resistor. + + You can monitor your PSU voltage using liveplotter in odrivetool + by entering `start_liveplotter(lambda: [odrv0.vbus_voltage])`. If + during a move you see the voltage rise above your PSU’s nominal + set voltage then you have your brake resistance set too low. This + may happen if you are using long wires or small gauge wires to + connect your brake resistor to your odrive which will added extra + resistance. This extra resistance needs to be accounted for to + prevent this voltage spike. If you have checked all your + connections you can also try increasing your brake resistance by + ~ 0.01 Ohm at a time to a maximum of 0.05 greater than your brake + resistor value. + CurrentMeasurementTimeout: + BrakeResistorDisarmed: + doc: The brake resistor was unexpectedly disarmed. + MotorDisarmed: + doc: The motor was unexpectedly disarmed. + MotorFailed: + doc: Check `motor.error` for more information. + SensorlessEstimatorFailed: + EncoderFailed: + doc: Check `encoder.error` for more information. + ControllerFailed: + PosCtrlDuringSensorless: + status: deprecated + WatchdogTimerExpired: + MinEndstopPressed: + MaxEndstopPressed: + EstopRequested: + HomingWithoutEndstop: + bit: 17 + doc: the min endstop was not enabled during homing + OverTemp: + doc: Check `fet_thermistor.error` and `motor_thermistor.error` for more information. + step_dir_active: readonly bool + current_state: readonly AxisState + requested_state: AxisState + loop_counter: readonly uint32 + lockin_state: + typeargs: {fibre.Property.mode: readonly} + values: + Inactive: + Ramp: + Accelerate: + ConstVel: + is_homed: {type: bool, c_name: homing_.is_homed} + config: + c_is_class: False + attributes: + startup_motor_calibration: + type: bool + doc: run motor calibration at startup, skip otherwise + startup_encoder_index_search: + type: bool + doc: run encoder index search after startup, skip otherwise this only has an effect if encoder.config.use_index is also true + startup_encoder_offset_calibration: + type: bool + doc: run encoder offset calibration after startup, skip otherwise + startup_closed_loop_control: + type: bool + doc: enable closed loop control after calibration/startup + startup_sensorless_control: + type: bool + doc: enable sensorless control after calibration/startup + startup_homing: + type: bool + doc: enable homing after calibration/startup + enable_step_dir: + type: bool + doc: Enable step/dir input after calibration. + For M0 this has no effect if `config.enable_uart` is true. + step_dir_always_on: + type: bool + doc: Keep step/dir enabled while the motor is disabled. + This is ignored if enable_step_dir is false. + This setting only takes effect on a state transition + into idle or out of closed loop control. + turns_per_step: float32 + watchdog_timeout: + type: float32 + unit: s + doc: 0 disables watchdog + enable_watchdog: bool + step_gpio_pin: {type: uint16, c_setter: 'set_step_gpio_pin'} + dir_gpio_pin: {type: uint16, c_setter: 'set_dir_gpio_pin'} + calibration_lockin: # TODO: this is a subset of lockin state + c_is_class: False + attributes: + current: float32 + ramp_time: float32 + ramp_distance: float32 + accel: float32 + vel: float32 + sensorless_ramp: LockinConfig + general_lockin: LockinConfig + can_node_id: + type: uint32 + doc: Both axes will have the same id to start + can_node_id_extended: bool + can_heartbeat_rate_ms: uint32 + fet_thermistor: OnboardThermistorCurrentLimiter + motor_thermistor: OffboardThermistorCurrentLimiter + motor: Motor + controller: Controller + encoder: Encoder + sensorless_estimator: SensorlessEstimator + trap_traj: TrapezoidalTrajectory + min_endstop: Endstop + max_endstop: Endstop + functions: + watchdog_feed: + doc: Feed the watchdog to prevent watchdog timeouts. + clear_errors: + doc: Check the watchdog timer for expiration. Also sets the watchdog error bit if expired. + + ODrive.Axis.LockinConfig: + c_is_class: False + attributes: + current: + type: float32 + unit: A + ramp_time: + type: float32 + unit: s + ramp_distance: + type: float32 + unit: rad + accel: + type: float32 + unit: rad/s^2 + vel: + type: float32 + unit: rad/s + finish_distance: + type: float32 + unit: rad + finish_on_vel: bool + finish_on_distance: bool + finish_on_enc_idx: bool + + ODrive.ThermistorCurrentLimiter: + c_is_class: False + + ODrive.OnboardThermistorCurrentLimiter: + c_is_class: True + attributes: + error: ThermistorCurrentLimiter.Error + temperature: readonly float32 + config: + c_is_class: False + attributes: + temp_limit_lower: + type: float32 + doc: The lower limit when the controller starts limiting current. + temp_limit_upper: + type: float32 + doc: The upper limit when current limit reaches 0 Amps and an over temperature error is triggered. + enabled: {type: bool, doc: Whether this thermistor is enabled. } + + ODrive.OffboardThermistorCurrentLimiter: + c_is_class: True + attributes: + error: ThermistorCurrentLimiter.Error + temperature: readonly float32 + config: + c_is_class: False + attributes: + gpio_pin: {type: uint16, c_setter: set_gpio_pin} + poly_coefficient_0: {type: float32, c_name: 'thermistor_poly_coeffs[0]'} + poly_coefficient_1: {type: float32, c_name: 'thermistor_poly_coeffs[1]'} + poly_coefficient_2: {type: float32, c_name: 'thermistor_poly_coeffs[2]'} + poly_coefficient_3: {type: float32, c_name: 'thermistor_poly_coeffs[3]'} + temp_limit_lower: + type: float32 + doc: The lower limit when the controller starts limiting current. + temp_limit_upper: + type: float32 + doc: The upper limit when current limit reaches 0 Amps and an over temperature error is triggered. + enabled: {type: bool, doc: Whether this thermistor is enabled. } + + ODrive.Motor: + c_is_class: True + attributes: + error: + nullflag: None + flags: + PhaseResistanceOutOfRange: + brief: The measured motor phase resistance is outside of the plausible range. + doc: | + During calibration the motor resistance and + [inductance](https://en.wikipedia.org/wiki/Inductance) is measured. + If the measured motor resistance or inductance falls outside a set + range this error will be returned. Check that all motor leads are + connected securely. + + The measured values can be viewed using odrivetool as is shown below: + ``` + In [2]: odrv0.axis0.motor.config.phase_inductance + Out[2]: 1.408751450071577e-05 + + In [3]: odrv0.axis0.motor.config.phase_resistance + Out[3]: 0.029788672924041748 + ``` + Some motors will have a considerably different phase resistance + and inductance than this. For example, gimbal motors, some small + motors (e.g. < 10A peak current). If you think this applies to you + try increasing `config.resistance_calib_max_voltage` from + its default value of 1 using odrivetool and repeat the motor + calibration process. If your motor has a small peak current draw + (e.g. < 20A) you can also try decreasing + `config.calibration_current` from its default value of 10A. + + In general, you need + ```text + resistance_calib_max_voltage > calibration_current * phase_resistance + resistance_calib_max_voltage < 0.5 * vbus_voltage + ``` + PhaseInductanceOutOfRange: + brief: The measured motor phase inductance is outside of the plausible range. + doc: | + See `PhaseResistanceOutOfRange` for details. + AdcFailed: + DrvFault: + brief: The gate driver chip reported an error. + doc: | + The ODrive v3.4 is known to have a hardware issue whereby the + motors would stop operating when applying high currents to M0. The + reported error of both motors in this case is `ERROR_DRV_FAULT`. + + The conjecture is that the high switching current creates large + ripples in the power supply of the DRV8301 gate driver chips, thus + tripping its under-voltage fault detection. + + To resolve this issue you can limit the M0 current to 40A. The + lowest current at which the DRV fault was observed is 45A on one + test motor and 50A on another test motor. Refer to + [this post](https://discourse.odriverobotics.com/t/drv-fault-on-odrive-v3-4/558) + for instructions for a hardware fix. + ControlDeadlineMissed: + NotImplementedMotorType: + BrakeCurrentOutOfRange: + ModulationMagnitude: + doc: | + The bus voltage was insufficent to push the requested current + through the motor. + If you are getting this during motor calibration, make sure that + `config.resistance_calib_max_voltage` is no more than half + your bus voltage. + + For gimbal motors, it is recommended to set the + `config.calibration_current` and `config.current_lim` + to half your bus voltage, or less. + BrakeDeadtimeViolation: + UnexpectedTimerCallback: + CurrentSenseSaturation: + CurrentLimitViolation: {bit: 12} + BrakeDutyCycleNan: + DcBusOverRegenCurrent: {doc: too much current pushed into the power supply} + DcBusOverCurrent: {doc: too much current pulled out of the power supply} + armed_state: + typeargs: {fibre.Property.mode: readonly} + values: + Disarmed: + WaitingForTimings: + WaitingForUpdate: + Armed: + is_calibrated: readonly bool + current_meas_phB: {type: readonly float32, c_name: current_meas_.phB} + current_meas_phC: {type: readonly float32, c_name: current_meas_.phC} + DC_calib_phB: {type: float32, c_name: DC_calib_.phB} + DC_calib_phC: {type: float32, c_name: DC_calib_.phC} + phase_current_rev_gain: float32 + effective_current_lim: readonly float32 + current_control: + c_is_class: False + attributes: + p_gain: float32 + i_gain: float32 + v_current_control_integral_d: float32 + v_current_control_integral_q: float32 + Ibus: float32 + final_v_alpha: float32 + final_v_beta: float32 + Id_setpoint: float32 + Iq_setpoint: readonly float32 + Iq_measured: float32 + Id_measured: float32 + I_measured_report_filter_k: float32 + max_allowed_current: readonly float32 + overcurrent_trip_level: readonly float32 + acim_rotor_flux: float32 + async_phase_vel: readonly float32 + async_phase_offset: float32 + gate_driver: + c_name: gate_driver_exported_ + c_is_class: False + attributes: + drv_fault: + typeargs: {fibre.Property.mode: readonly} + nullflag: NoFault + flags: + FetLowCOvercurrent: {bit: 0, doc: FET Low side, Phase C Over Current fault} + FetHighCOvercurrent: {bit: 1, doc: FET High side, Phase C Over Current fault} + FetLowBOvercurrent: {bit: 2, doc: FET Low side, Phase B Over Current fault} + FetHighBOvercurrent: {bit: 3, doc: FET High side, Phase B Over Current fault} + FetLowAOvercurrent: {bit: 4, doc: FET Low side, Phase A Over Current fault} + FetHighAOvercurrent: {bit: 5, doc: FET High side, Phase A Over Current fault} + OvertemperatureWarning: {bit: 6, doc: Over Temperature Warning fault} + OvertemperatureShutdown: {bit: 7, doc: Over Temperature Shut Down fault} + PVddUndervoltage: {bit: 8, doc: Power supply Vdd Under Voltage fault} + GVddUndervoltage: {bit: 9, doc: DRV8301 Vdd Under Voltage fault} + GVddOvervoltage: {bit: 10, doc: DRV8301 Vdd Over Voltage fault} + # status_reg_1: readonly uint32 + # status_reg_2: readonly uint32 + # ctrl_reg_1: readonly uint32 + # ctrl_reg_2: readonly uint32 + timing_log: + c_is_class: False + attributes: + general: {type: readonly uint16, c_name: 'get(TIMING_LOG_GENERAL)'} + adc_cb_i: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_I)'} + adc_cb_dc: {type: readonly uint16, c_name: 'get(TIMING_LOG_ADC_CB_DC)'} + meas_r: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_R)'} + meas_l: {type: readonly uint16, c_name: 'get(TIMING_LOG_MEAS_L)'} + enc_calib: {type: readonly uint16, c_name: 'get(TIMING_LOG_ENC_CALIB)'} + idx_search: {type: readonly uint16, c_name: 'get(TIMING_LOG_IDX_SEARCH)'} + foc_voltage: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_VOLTAGE)'} + foc_current: {type: readonly uint16, c_name: 'get(TIMING_LOG_FOC_CURRENT)'} + spi_start: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_START)'} + sample_now: {type: readonly uint16, c_name: 'get(TIMING_LOG_SAMPLE_NOW)'} + spi_end: {type: readonly uint16, c_name: 'get(TIMING_LOG_SPI_END)'} + config: + c_is_class: False + attributes: + pre_calibrated: {type: bool, c_setter: set_pre_calibrated} + pole_pairs: int32 + calibration_current: float32 + resistance_calib_max_voltage: float32 + phase_inductance: {type: float32, c_setter: set_phase_inductance} + phase_resistance: {type: float32, c_setter: set_phase_resistance} + torque_constant: float32 + direction: int32 + motor_type: MotorType + current_lim: float32 + current_lim_margin: float32 + torque_lim: float32 + inverter_temp_limit_lower: float32 + inverter_temp_limit_upper: float32 + requested_current_range: float32 + current_control_bandwidth: {type: float32, c_setter: set_current_control_bandwidth} + acim_slip_velocity: float32 + acim_gain_min_flux: float32 + acim_autoflux_min_Id: float32 + acim_autoflux_enable: bool + acim_autoflux_attack_gain: float32 + acim_autoflux_decay_gain: float32 + + + ODrive.Controller: + c_is_class: True + attributes: + error: + nullflag: None + flags: + Overspeed: + doc: | + Try increasing `config.vel_limit`. The default of 2 turns per second + gives a motor speed of only 120 RPM. Note: Even if + you do not commanded your motor to exceed `config.vel_limit` + sudden changes in the load placed on a motor may cause this speed + to be temporarily exceeded, resulting in this error. + + You can also try increasing `config.vel_limit_tolerance`. The + default value of 1.2 means it will only allow a 20% violation of + the speed limit. You can set the `config.vel_limit_tolerance` to 0 + to disable the check altogether. + InvalidInputMode: + UnstableGain: + InvalidMirrorAxis: + InvalidLoadEncoder: + InvalidEstimate: + input_pos: + type: float32 + unit: turn + c_setter: set_input_pos + input_vel: + type: float32 + unit: turn/s + input_torque: float32 + pos_setpoint: readonly float32 + vel_setpoint: readonly float32 + torque_setpoint: readonly float32 + trajectory_done: readonly bool + vel_integrator_torque: float32 + anticogging_valid: bool + config: + c_is_class: False + attributes: + gain_scheduling_width: float32 + enable_vel_limit: bool + enable_current_mode_vel_limit: + type: bool + doc: Enable velocity limit in current control mode (requires a valid velocity estimator). + enable_gain_scheduling: bool + enable_overspeed_error: bool + control_mode: ControlMode + input_mode: InputMode + pos_gain: + type: float32 + unit: (turn/s) / turn + vel_gain: + type: float32 + unit: 'Nm/(turn/s)' + vel_integrator_gain: + type: float32 + unit: Nm/(turn/s * s) + vel_limit: + type: float32 + unit: turn/s + doc: Infinity to disable. + vel_limit_tolerance: + type: float32 + doc: Ratio to `vel_limit`. Infinity to disable. + vel_ramp_rate: float32 + torque_ramp_rate: + type: float32 + unit: Nm / sec + circular_setpoints: + type: bool + circular_setpoint_range: + type: float32 + doc: circular range in [turns] for position setpoints when circular_setpoints is True + homing_speed: + type: float32 + unit: turns/s + inertia: + type: float32 + unit: Nm/(turn/s^2) + axis_to_mirror: uint8 + mirror_ratio: float32 + load_encoder_axis: + type: uint8 + # TODO: this is meaningless for a user. Should there be a separate developer note? + doc: Default depends on Axis number and is set in load_configuration() + input_filter_bandwidth: + type: float32 + unit: 1/s + c_setter: set_input_filter_bandwidth + anticogging: + c_is_class: False + attributes: + index: readonly uint32 + pre_calibrated: bool + calib_anticogging: readonly bool + calib_pos_threshold: float32 + calib_vel_threshold: float32 + cogging_ratio: readonly float32 + anticogging_enabled: bool + functions: + move_incremental: + doc: Moves the axes' goal point by a specified increment. + in: + displacement: {type: float32, doc: The desired position change.} + from_input_pos: {type: bool, doc: + 'If true, the increment is applied relative to `input_pos`. + If false, the increment is applied relative to `pos_setpoint`, which + usually corresponds roughly to the current position of the axis.' + } + start_anticogging_calibration: + + + ODrive.Encoder: + c_is_class: True + attributes: + error: + nullflag: None + flags: + UnstableGain: + CprPolepairsMismatch: + doc: | + Confirm you have entered the correct count per rotation (CPR) for + [your encoder](https://docs.odriverobotics.com/encoders). The + ODrive uses your supplied value for the motor pole pairs to + measure the CPR. So you should also double check this value. + + Note that the AMT encoders are configurable using the micro- + switches on the encoder PCB and so you may need to check that + these are in the right positions. If your encoder lists its pulse + per rotation (PPR) multiply that number by four to get CPR. + NoResponse: + doc: | + Confirm that your encoder is plugged into the right pins on the + ODrive board. + UnsupportedEncoderMode: + IllegalHallState: + IndexNotFoundYet: + doc: | + Check that your encoder is a model that has an index pulse. If + your encoder does not have a wire connected to pin Z on your + ODrive then it does not output an index pulse. + AbsSpiTimeout: + AbsSpiComFail: + AbsSpiNotReady: + is_ready: readonly bool + index_found: readonly bool + shadow_count: readonly int32 + count_in_cpr: readonly int32 + interpolation: readonly float32 + phase: readonly float32 + pos_estimate: readonly float32 + pos_estimate_counts: readonly float32 + pos_cpr: readonly float32 + pos_cpr_counts: readonly float32 + pos_circular: readonly float32 + hall_state: readonly uint8 + vel_estimate: readonly float32 + vel_estimate_counts: readonly float32 + calib_scan_response: readonly float32 + pos_abs: int32 + spi_error_rate: readonly float32 + config: + c_is_class: False + attributes: + mode: Mode + use_index: {type: bool, c_setter: set_use_index} + find_idx_on_lockin_only: {type: bool, c_setter: set_find_idx_on_lockin_only} + abs_spi_cs_gpio_pin: {type: uint16, c_setter: set_abs_spi_cs_gpio_pin} + zero_count_on_find_idx: bool + cpr: int32 + offset: int32 + pre_calibrated: {type: bool, c_setter: set_pre_calibrated} + offset_float: float32 + enable_phase_interpolation: bool + bandwidth: {type: float32, c_setter: set_bandwidth} + calib_range: float32 + calib_scan_distance: float32 + calib_scan_omega: float32 + idx_search_unidirectional: bool + ignore_illegal_hall_state: bool + sincos_gpio_pin_sin: uint16 + sincos_gpio_pin_cos: uint16 + functions: + set_linear_count: {in: {count: int32}} + + + ODrive.SensorlessEstimator: + c_is_class: True + attributes: + error: + nullflag: None + flags: + UnstableGain: + phase: float32 + pll_pos: float32 + vel_estimate: float32 + # pll_kp: float32 + # pll_ki: float32 + config: + c_is_class: False + attributes: + observer_gain: float32 + pll_bandwidth: float32 + pm_flux_linkage: float32 + + + ODrive.TrapezoidalTrajectory: + c_is_class: True + attributes: + config: + c_is_class: False + attributes: + vel_limit: float32 + accel_limit: float32 + decel_limit: float32 + + + ODrive.Endstop: + c_is_class: True + attributes: + endstop_state: readonly bool + config: + c_is_class: False + attributes: + gpio_num: {type: uint16, c_setter: set_gpio_num} + enabled: {type: bool, c_setter: set_enabled} + offset: float32 + is_active_high: bool + pullup: bool + debounce_ms: {type: uint32, c_setter: set_debounce_ms} + + +valuetypes: + ODrive.Can.Protocol: + values: {Simple: } + + ODrive.Axis.AxisState: # TODO: remove redundant "Axis" in name + values: + Undefined: + doc: will fall through to idle + Idle: + brief: Disable motor PWM and do nothing. + StartupSequence: + brief: Run the startup procedure. + doc: the actual sequence is defined by the `config`.startup... flags + FullCalibrationSequence: + doc: Run motor calibration and then encoder offset calibration (or encoder + index search if `.encoder.config.use_index` is `True`). + MotorCalibration: + brief: Measure phase resistance and phase inductance of the motor. + doc: | + * To store the results set `motor.config.pre_calibrated` to `True` + and save the configuration (`save_configuration()`). After that you + don't have to run the motor calibration on the next start up. + * This modifies the variables `motor.config.phase_resistance` and + `motor.config.phase_inductance`. + SensorlessControl: + brief: Run sensorless control. + doc: | + * The motor must be calibrated (`motor.is_calibrated`) + * `controller.config.control_mode` must be `True`. + EncoderIndexSearch: + brief: Turn the motor in one direction until the encoder index is traversed. + doc: This state can only be entered if `encoder.config.use_index` is `True`. + EncoderOffsetCalibration: + brief: Turn the motor in one direction for a few seconds and then back to measure the offset between the encoder position and the electrical phase. + doc: | + * Can only be entered if the motor is calibrated (`motor.is_calibrated`). + * A successful encoder calibration will make the `encoder.is_ready` + go to true. + ClosedLoopControl: + brief: Run closed loop control. + doc: | + * The action depends on the `controller.config.control_mode`. + * Can only be entered if the motor is calibrated + (`motor.is_calibrated`) and the encoder is ready (`encoder.is_ready`). + LockinSpin: + brief: Run lockin spin. + doc: | + Can only be entered if the motor is calibrated (`motor.is_calibrated`) + or the motor direction is unspecified (`motor.config.direction` == 1) + EncoderDirFind: + brief: Run encoder direction search. + doc: | + Can only be entered if the motor is calibrated (`motor.is_calibrated`). + Homing: + brief: Run axis homing function. + doc: + Endstops must be enabled to use this feature. + + ODrive.ThermistorCurrentLimiter.Error: + nullflag: None + flags: + OverTemp: + doc: The thermistor temperature upper limit was exceeded. + + ODrive.Encoder.Mode: + values: + Incremental: + Hall: + Sincos: + SpiAbsCui: + value: 0x100 + doc: compatible with CUI AMT23xx + SpiAbsAms: + value: 0x101 + doc: compatible with AMS AS5047P, AS5048A/AS5048B (no daisy chain support) + SpiAbsAeat: + value: 0x102 + doc: not yet implemented + SpiAbsRls: + value: 0x103 + doc: RLS Encoders + + ODrive.Controller.ControlMode: + values: + # Note: these should be sorted from lowest level of control to + # highest level of control, to allow "<" style comparisons. + VoltageControl: + doc: this one is not normally used + TorqueControl: + VelocityControl: + PositionControl: + + ODrive.Controller.InputMode: + values: + Inactive: + brief: Disable inputs. Setpoints retain their last value. + Passthrough: + brief: Pass `input_xxx` through to `xxx_setpoint` directly. + doc: | + ### Valid Inputs: + * `input_pos` + * `input_vel` + * `input_current` + + ### Valid Control modes: + * `CONTROL_MODE_VOLTAGE_CONTROL` + * `CONTROL_MODE_TORQUE_CONTROL` + * `CONTROL_MODE_VELOCITY_CONTROL` + * `CONTROL_MODE_POSITION_CONTROL` + VelRamp: + brief: Ramps a velocity command from the current value to the target value. + doc: | + ### Configuration Values: + * `config.vel_ramp_rate` [turn/sec] + * `config.inertia` [Nm/(turn/s^2))] + + ### Valid inputs: + * `input_vel` + + ### Valid Control Modes: + * `CONTROL_MODE_VELOCITY_CONTROL` + PosFilter: + brief: Implements a 2nd order position tracking filter. + doc: | + Intended for use with step/dir interface, but can also be used with + position-only commands. + + ![POS Filter Response](../secondOrderResponse.PNG) + Result of a step command from 1000 to 0 + + ### Configuration Values: + * `config.input_filter_bandwidth` + * `config.inertia` + + ### Valid inputs: + * `input_pos` + + ### Valid Control modes: + * `CONTROL_MODE_POSITION_CONTROL` + MixChannels: + brief: Not Implemented. + TrapTraj: + brief: Implementes an online trapezoidal trajectory planner. + doc: | + ![Trapezoidal Planner Response](../TrapTrajPosVel.PNG) + + ### Configuration Values: + * `trap_traj.config.vel_limit` + * `trap_traj.config.accel_limit` + * `trap_traj.config.decel_limit` + * `config.inertia` + + ### Valid Inputs: + * `input_pos` + + ### Valid Control Modes: + * `CONTROL_MODE_POSITION_CONTROL` + TorqueRamp: + brief: Ramp a torque command from the current value to the target value. + doc: | + ### Configuration Values: + * `config.torque_ramp_rate` + + ### Valid Inputs: + * `input_current` + + ### Valid Control Modes: + * `CONTROL_MODE_TORQUE_CONTROL` + Mirror: + brief: Implements "electronic mirroring". + doc: | + This is like electronic camming, but you can only mirror exactly the + movements of the other motor, according to a fixed ratio. + + [![](http://img.youtube.com/vi/D4_vBtyVVzM/0.jpg)](http://www.youtube.com/watch?v=D4_vBtyVVzM "Example Mirroring Video") + + ### Configuration Values + * `config.axis_to_mirror` + * `config.mirror_ratio` + + ### Valid Inputs + * None. Inputs are taken directly from the other axis encoder estimates + + ### Valid Control modes + * `CONTROL_MODE_POSITION_CONTROL` + + ODrive.Motor.MotorType: + values: + HighCurrent: + #LowCurrent: # not implemented + Gimbal: {value: 2} + Acim: \ No newline at end of file diff --git a/Firmware/tup.config.default b/Firmware/tup.config.default index 5c2c4822..b2d49106 100644 --- a/Firmware/tup.config.default +++ b/Firmware/tup.config.default @@ -4,6 +4,7 @@ CONFIG_USB_PROTOCOL=native CONFIG_UART_PROTOCOL=ascii CONFIG_DEBUG=false +CONFIG_DOCTEST=false # Uncomment this to error on compilation warnings #CONFIG_STRICT=true diff --git a/ODrive_Workspace.code-workspace b/ODrive_Workspace.code-workspace index 3d990eaf..8c1fe5c3 100644 --- a/ODrive_Workspace.code-workspace +++ b/ODrive_Workspace.code-workspace @@ -8,20 +8,15 @@ }, { "path": "docs" + }, + { + "path": "analysis" } ], "settings": { - - "c-cpp-flylint.cppcheck.includePaths": [ - "${workspaceRoot}", - "${workspaceRoot}/fibre/cpp/include/fibre", - "${workspaceRoot}/communication", - "${workspaceRoot}/MotorControl", - ], - "c-cpp-flylint.cppcheck.platform": "avr8", "c-cpp-flylint.cppcheck.standard": ["c99","c++14"], - "files.associations": { + "*.config": "yaml", "memory": "cpp", "utility": "cpp", "deque": "cpp", @@ -55,7 +50,19 @@ "chrono": "cpp", "condition_variable": "cpp", "future": "cpp", - "arm_math.h": "c" + "arm_math.h": "c", + "iostream": "cpp", + "cmath": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "ctime": "cpp", + "unordered_map": "cpp", + "fstream": "cpp", + "iomanip": "cpp", + "optional": "cpp", + "sstream": "cpp", + "utils.h": "c" } } } diff --git a/README.md b/README.md index 915c1636..eb3cea15 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This project is all about accurately driving brushless motors, for cheap. The ai | master | [![Build Status](https://travis-ci.org/madcowswe/ODrive.png?branch=master)](https://travis-ci.org/madcowswe/ODrive) | | devel | [![Build Status](https://travis-ci.org/madcowswe/ODrive.png?branch=devel)](https://travis-ci.org/madcowswe/ODrive) | +[![pip install odrive (nightly)](https://github.com/madcowswe/ODrive/workflows/pip%20install%20odrive%20(nightly)/badge.svg)](https://github.com/madcowswe/ODrive/actions?query=workflow%3A%22pip+install+odrive+%28nightly%29%22) Please refer to the [Developer Guide](https://docs.odriverobotics.com/developer-guide) to get started with ODrive firmware development. diff --git a/analysis/Simulation/TranslationalMass.py b/analysis/Simulation/TranslationalMass.py new file mode 100644 index 00000000..3cb1a4aa --- /dev/null +++ b/analysis/Simulation/TranslationalMass.py @@ -0,0 +1,35 @@ +import os +import matplotlib.pyplot as plt +from control.matlab import * + +# Input: Current (A) +# Output: Torque (Nm) +# Params: Kt (Nm/A) +def motor(Kt): + return tf(Kt, 1) + +# Mass-Spring-Damper +# Input: Force +# Output: Position +# Params: m (kg) +# b +# k (N/m) +def mass(m, b, k): + A = [[0, 1.], [-k/m, -b/m]] + B = [[0], [1/m]] + C = [[1., 0]] + return ss(A, B, C, 0) + +# Input: Torque (Nm) +# Output: Force (N) +# Params: r (m) +def pulley(r): + return tf(r, 1) + +sys = series(motor(2.5), pulley(0.015), mass(0.10, 0, 0)) +yout, T, xout = step(sys, return_x=True) +print(yout) +# plt.plot(T, yout) +plt.plot(T, xout) +plt.legend(['Displacement', 'Velocity']) +plt.show() \ No newline at end of file diff --git a/analysis/filterpoles.py b/analysis/filterpoles.py new file mode 100644 index 00000000..ca57bc8f --- /dev/null +++ b/analysis/filterpoles.py @@ -0,0 +1,90 @@ + +import numpy as np +import sympy as sp +from scipy.integrate import solve_ivp +import matplotlib.pyplot as plt + +do_mass_spring = True +do_PLL = False + +bandwidth = 10 + +pos_ref = 0 +vel_ref = 0 +init_pos = 1000 +init_vel = 0 + +plotend = 1 +plotfrequency = 1000.0 + +fig, ax1 = plt.subplots() + +if do_mass_spring: + # 2nd order system response with manipulation of velocity only + # This is similar to a mass/spring/damper system + # pos_dot = vel + # vel_dot = Kp * delta_pos + Ki * delta_vel + + Ki = 2.0 * bandwidth + Kp = 0.25 * Ki**2 + + def get_Xdot(t, X): + pos = X[0] + vel = X[1] + + pos_err = pos_ref - pos + vel_err = vel_ref - vel + + pos_dot = vel + vel_dot = Kp * pos_err + Ki * vel_err + + Xdot = [pos_dot, vel_dot] + return Xdot + + sol = solve_ivp(get_Xdot, (0.0, plotend), [init_pos, init_vel], t_eval=np.linspace(0, plotend, plotend*plotfrequency)) + + color = 'tab:red' + ax1.set_xlabel('time (s)') + ax1.set_ylabel('pos', color=color) + ax1.plot(np.transpose(sol.t), np.transpose(sol.y[0,:]), label='physical mass', color=color) + ax1.tick_params(axis='y', labelcolor=color) + + ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis + + color = 'tab:blue' + ax2.set_ylabel('vel', color=color) # we already handled the x-label with ax1 + ax2.plot(np.transpose(sol.t), np.transpose(sol.y[1,:]), label='physical mass', color=color) + ax2.tick_params(axis='y', labelcolor=color) + + +if do_PLL: + # 2nd order system response with a "slipping displacement" term directly on position + # This formulation is given in the sensorless PLL paper + # pos_dot = vel + Kp * delta_pos + # vel_dot = Ki * delta_pos + + Kp = 2.0 * bandwidth + Ki = 0.25 * Kp**2 + + def get_Xdot(t, X): + pos = X[0] + vel = X[1] + + pos_err = pos_ref - pos + vel_err = vel_ref - vel + + pos_dot = vel + Kp * pos_err + vel_dot = Ki * pos_err + + Xdot = [pos_dot, vel_dot] + return Xdot + + sol = solve_ivp(get_Xdot, (0.0, plotend), [init_pos, init_vel], t_eval=np.linspace(0, plotend, plotend*plotfrequency)) + + plt.plot(np.transpose(sol.t), np.transpose(sol.y[0,:]), label='PLL pos') + plt.plot(np.transpose(sol.t), np.transpose(sol.y[1,:]), label='PLL vel') + + + +plt.legend() +plt.show(block=True) \ No newline at end of file diff --git a/analysis/motor_analysis/ac_induction_motor.py b/analysis/motor_analysis/ac_induction_motor.py new file mode 100644 index 00000000..ccc06ec8 --- /dev/null +++ b/analysis/motor_analysis/ac_induction_motor.py @@ -0,0 +1,245 @@ + +import numpy as np +import matplotlib.pyplot as plt +from scipy.integrate import solve_ivp +from scipy.optimize import least_squares +from engineering_notation import EngNumber + +filename = "oscilloscope.csv" + +PLOT_INITAL = True +DO_FITTING = False +PLOT_PROGRESS = False +REPORT_PROGRESS = True +assumed_rotor_resistance = 1 +pole_pairs = 2 + +class ACMotor(): + """ + Models an induction motor based on Eq 10 in [1]. + [1] https://pdfs.semanticscholar.org/4770/15e472da4c2e05e9ff8c1b921c76a938f786.pdf + + + Note: This model refers all rotor quantities to the stator, i.e. the + quantities are as if the motor had a winding ratio of k = 1. + """ + + # parameters: (name, range) + parameter_definitions = [ + ('stator_inductance', (0, np.inf), 'H'), # aka l_s, [Henry] + ('stator_resistance', (0, np.inf), 'ohm'), # aka r_s, [Ohm] + ('rotor_inductance', (0, np.inf), 'H'), # aka l_r [Henry] + # ('rotor_resistance', (0, np.inf), 'ohm'), # aka r_r [Ohm] + ('mutual_inductance_factor', (0, 1.0), ''), #[unitless] = l_m**2 / (l_s * l_r) + ] + # parameter index lookup + pl = {r[0]:i for i, r in enumerate(parameter_definitions)} + + # states: (name, initial_value) + state_definitions = [ + ('stator_current', 0.0), # aka i_s, [A] + ('rotor_flux', 0.0), # aka Phi_r, [Wb] + ] # complex numbers + # state index lookup + sl = {r[0]:i for i, r in enumerate(state_definitions)} + + def __init__(self, params): + self.params = params + + # Assigned in run(): + # self.stator_voltage = None + # self.omega_stator = None + # self.omega_rotor = None + + def get_mutual_inductance(self): + return np.sqrt( + self.params[ACMotor.pl['mutual_inductance_factor']] + * self.params[ACMotor.pl['stator_inductance']] + * self.params[ACMotor.pl['rotor_inductance']] + ) + + def system_function(self, t, y): + # local shorthand for params + p = self.params + pl = ACMotor.pl + sl = ACMotor.sl + + # rotor_resistance = p[pl['rotor_resistance']] + rotor_resistance = assumed_rotor_resistance + mutual_inductance = self.get_mutual_inductance() + + tau_rotor = p[pl['rotor_inductance']] / rotor_resistance # [s] + coupling_factor = mutual_inductance / p[pl['rotor_inductance']] # aka k_r [unitless] + r_sigma = p[pl['stator_resistance']] + coupling_factor**2 * rotor_resistance # [Ohm] + leakage_factor = 1.0 - mutual_inductance**2 / (p[pl['rotor_inductance']] * p[pl['stator_inductance']]) # aka sigma [unitless] + tau_stator_prime = leakage_factor * p[pl['stator_inductance']] / r_sigma # [s] + + # [1] Eq 10a + dstator_current_dt = ( + -1.0j * self.omega_stator * tau_stator_prime * y[sl['stator_current']] + - coupling_factor / (r_sigma * tau_rotor) * (1.0j*self.omega_rotor * tau_rotor - 1.0) * y[sl['rotor_flux']] + + 1.0 / r_sigma * self.stator_voltage + - y[sl['stator_current']] + ) / tau_stator_prime + + # [1] Eq 10b + drotor_flux_dt = ( + -1.0j * (self.omega_stator - self.omega_rotor) * tau_rotor * y[sl['rotor_flux']] + + mutual_inductance * y[sl['stator_current']] + - y[sl['rotor_flux']] + ) / tau_rotor + + return [dstator_current_dt, drotor_flux_dt] + + def run(self, time_series, voltage, omega_stator, omega_rotor): + self.stator_voltage = voltage + self.omega_stator = omega_stator + self.omega_rotor = omega_rotor + + y0 = np.array([x[1] for x in ACMotor.state_definitions], dtype=np.complex) + + result = solve_ivp(self.system_function, (time_series[0], time_series[-1]), y0, t_eval=time_series) + y = result.y + + # compute derived state + rotor_inductance = self.params[ACMotor.pl['rotor_inductance']] + rotor_current = (1/rotor_inductance) * (y[1] - self.get_mutual_inductance() * y[0]) + return np.vstack((y, rotor_current)) + + def print_parameter_info(self): + print() + print('Given parameters:') + print('rotor_resistance = {}ohm'.format(EngNumber(assumed_rotor_resistance))) + + print() + print('Fitted parameters:') + for i, r in enumerate(ACMotor.parameter_definitions): + print('{} = {}{}'.format(r[0], EngNumber(self.params[i]), r[2])) + + print() + print('Derived parameters:') + mutual_inductance = motor.get_mutual_inductance() + coupling_factor = mutual_inductance / self.params[ACMotor.pl['rotor_inductance']] + torque_constant = pole_pairs * coupling_factor * mutual_inductance + motor_constant = torque_constant / (3.0 * self.params[ACMotor.pl['stator_resistance']]) + print('mutual_inductance = {}H'.format(EngNumber(mutual_inductance))) + print('coupling_factor = {}'.format(EngNumber(coupling_factor))) + print('torque_constant = {}Nm/A^2'.format(EngNumber(torque_constant))) + print('motor_constant = {}Nm/W'.format(EngNumber(motor_constant))) + + def print_run_info(self, y): + final_stator_current_d = np.real(y[0,-1]) + final_stator_current_q = np.imag(y[0,-1]) + final_rotor_flux_d = np.real(y[1,-1]) + + mutual_inductance = self.get_mutual_inductance() + coupling_factor = mutual_inductance / self.params[ACMotor.pl['rotor_inductance']] + final_torque_per_q_amp = pole_pairs * coupling_factor * final_rotor_flux_d + + print() + print('Final values:') + print('final_rotor_flux_d = {}Wb'.format(EngNumber(final_rotor_flux_d))) + print('final_stator_current_d = {}A'.format(EngNumber(final_stator_current_d))) + print('final_stator_current_q = {}A'.format(EngNumber(final_stator_current_q))) + print('final_torque_per_q_amp = {}Nm/A'.format(EngNumber(final_torque_per_q_amp))) + +def plot_data(t, y, ref, title): + fig, (ax1, ax2) = plt.subplots(2, sharex=True) + ax1b = ax1.twinx() + ax1.plot(t, ref, label='Measured current') + ax1.plot(t, np.real(y[0]), label='Stator current (d)') + ax1.plot(t, np.imag(y[0]), label='Stator current (q)') + ax2.plot(t, np.real(y[2]), label='Rotor current (d)') + ax2.plot(t, np.imag(y[2]), label='Rotor current (q)') + ax1b.plot(t, 1000*np.real(y[1]), 'C3', label='Rotor flux (d)') + ax1b.plot(t, 1000*np.imag(y[1]), 'C4', label='Rotor flux (q)') + ax1.set_xlabel('time [s]') + ax1.set_ylabel('Current [A]') + ax1b.set_ylabel('Flux [mWb]') + ax2.set_ylabel('Current [A]') + plt.title(title) + fig.legend() + plt.show() + +# load test data +t = np.arange(4096)/8000.0 +voltage_step = 1.0 +with open(filename, 'r') as fp: + test_response = np.array([float(x) for x in fp.readlines()]) + + +inital_parameters = np.zeros(len(ACMotor.parameter_definitions)) +inital_parameters[ACMotor.pl['stator_inductance']] = 7.72181086e-04 +inital_parameters[ACMotor.pl['stator_resistance']] = 3.06884624e-02 +inital_parameters[ACMotor.pl['rotor_inductance']] = assumed_rotor_resistance*6.82013522e-02 +# inital_parameters[ACMotor.pl['rotor_resistance']] = 1.0e-0 +# inital_parameters[ACMotor.pl['mutual_inductance']] = 2.40e-4 +inital_parameters[ACMotor.pl['mutual_inductance_factor']] = 8.68671978e-01 + +# inital_parameters[ACMotor.pl['stator_resistance']] = 1.298 +# inital_parameters[ACMotor.pl['stator_inductance']] = 0.157228647 +# inital_parameters[ACMotor.pl['rotor_resistance']] = 0.975052932 +# inital_parameters[ACMotor.pl['rotor_inductance']] = 0.16674423623999998 +# inital_parameters[ACMotor.pl['mutual_inductance']] = 0.157221177 + +# Plot initial run +if PLOT_INITAL: + print() + print('Initial run:') + motor = ACMotor(inital_parameters) + motor.print_parameter_info() + + y = motor.run( + time_series = t, + voltage = voltage_step, + omega_stator = 0, + omega_rotor= 0) + motor.print_run_info(y) + plot_data(t, y, test_response, 'initial') + + +# Fit to data +def get_residuals(params): + if REPORT_PROGRESS: print(params) + motor = ACMotor(params) + y = motor.run( + time_series = t, + voltage = voltage_step, + omega_stator = 0, + omega_rotor= 0) + + residuals = test_response - np.real(y[0]) + fitness = sum(residuals**2) + if REPORT_PROGRESS: print(fitness) + + if PLOT_PROGRESS: + plot_data(t, y, test_response, 'progress') + + return residuals + +if DO_FITTING: + print() + print('Fitting parameters:') + optiresult = least_squares(get_residuals, inital_parameters, + bounds=list(zip(*[x[1] for x in ACMotor.parameter_definitions])), + x_scale='jac', + diff_step = 1e-2 * np.array([ + 7.58192590e-04, + 3.07166671e-02, + 6.85207075e-02, + # 4.66461518e+00, + 8.67540012e-01]) + ) + print(optiresult.message) + + motor = ACMotor(optiresult.x) + motor.print_parameter_info() + + y = motor.run( + time_series = t, + voltage = voltage_step, + omega_stator = 0, + omega_rotor= 0) + motor.print_run_info(y) + plot_data(t, y, test_response, 'final') + diff --git a/analysis/thermistors.py b/analysis/thermistors.py index 2a3792a9..5d6a30e2 100644 --- a/analysis/thermistors.py +++ b/analysis/thermistors.py @@ -1,32 +1,9 @@ #%% -import matplotlib.pyplot as plt -import numpy as np +from odrive.utils import calculate_thermistor_coeffs Rload = 3300 R_25 = 10000 -T_25 = 25 + 273.15 #Kelvin Beta = 3434 Tmin = 0 Tmax = 140 - -temps = np.linspace(Tmin, Tmax, 1000) -tempsK = temps + 273.15 - -# https://en.wikipedia.org/wiki/Thermistor#B_or_%CE%B2_parameter_equation -r_inf = R_25 * np.exp(-Beta/T_25) -R_temps = r_inf * np.exp(Beta/tempsK) -V = Rload / (Rload + R_temps) - -fit = np.polyfit(V, temps, 3) -p1 = np.poly1d(fit) -fit_temps = p1(V) - -#%% -print(fit) - -plt.plot(V, temps, label='actual') -plt.plot(V, fit_temps, label='fit') -plt.xlabel('normalized voltage') -plt.ylabel('Temp [C]') -plt.legend(loc=0) -plt.show() \ No newline at end of file +calculate_thermistor_coeffs(3, Rload, R_25, Beta, Tmin, Tmax, True) diff --git a/dockerbuild.sh b/dockerbuild.sh new file mode 100755 index 00000000..779b9bf1 --- /dev/null +++ b/dockerbuild.sh @@ -0,0 +1,47 @@ +function cleanup { + echo "Removing previous build artifacts" + rm -rf build + docker rm odrive-build-cont +} + +function gc { + cleanup + docker rmi odrive-build-img + docker image prune +} + +function build { + cleanup + + echo "Building the firmware" + docker build -t odrive-build-img . + + echo "Create container" + docker create --name odrive-build-cont odrive-build-img:latest + + echo "Extract build artifacts" + docker cp odrive-build-cont:ODrive/Firmware/build . +} + +function usage { + echo "usage: $0 (build | cleanup | gc)" + echo + echo "build -- build in docker and extract the artifacts." + echo "cleanup -- remove build artifacts from previous build" + echo "gc -- remove all build images and containers" +} + +case $1 in + build) + build + ;; + cleanup) + cleanup + ;; + gc) + gc + ;; + *) + usage + ;; +esac diff --git a/docs/Endstop_configuration.png b/docs/Endstop_configuration.png new file mode 100644 index 00000000..56a618ff Binary files /dev/null and b/docs/Endstop_configuration.png differ diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 860837ad..1490c10c 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,55 +1,57 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.2.9) - i18n (~> 0.7) + activesupport (6.0.3.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) + zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.11.1) colorator (1.1.0) - commonmarker (0.17.9) + commonmarker (0.17.13) ruby-enum (~> 0.5) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.6) + dnsruby (1.61.3) + addressable (~> 2.5) em-websocket (0.5.1) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) - ethon (0.11.0) + ethon (0.12.0) ffi (>= 1.3.0) - eventmachine (1.2.5) + eventmachine (1.2.7) execjs (2.7.0) - faraday (0.14.0) + faraday (1.0.1) multipart-post (>= 1.2, < 3) - ffi (1.9.24) + ffi (1.12.2) forwardable-extended (2.6.0) - gemoji (3.0.0) - github-pages (181) - activesupport (= 4.2.9) - github-pages-health-check (= 1.4.0) - jekyll (= 3.7.4) - jekyll-avatar (= 0.5.0) + gemoji (3.0.1) + github-pages (206) + github-pages-health-check (= 1.16.1) + jekyll (= 3.8.7) + jekyll-avatar (= 0.7.0) jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.1.5) + jekyll-commonmark-ghpages (= 0.1.6) jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.9.3) + jekyll-feed (= 0.13.0) jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.9.4) - jekyll-mentions (= 1.3.0) - jekyll-optional-front-matter (= 0.3.0) + jekyll-github-metadata (= 2.13.0) + jekyll-mentions (= 1.5.1) + jekyll-optional-front-matter (= 0.3.2) jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.2.0) - jekyll-redirect-from (= 0.13.0) - jekyll-relative-links (= 0.5.3) - jekyll-remote-theme (= 0.2.3) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.15.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.1) jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.4.0) - jekyll-sitemap (= 1.2.0) - jekyll-swiss (= 0.4.0) + jekyll-seo-tag (= 2.6.1) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) jekyll-theme-architect (= 0.1.1) jekyll-theme-cayman (= 0.1.1) jekyll-theme-dinky (= 0.1.1) @@ -59,33 +61,32 @@ GEM jekyll-theme-midnight (= 0.1.1) jekyll-theme-minimal (= 0.1.1) jekyll-theme-modernist (= 0.1.1) - jekyll-theme-primer (= 0.5.3) + jekyll-theme-primer (= 0.5.4) jekyll-theme-slate (= 0.1.1) jekyll-theme-tactile (= 0.1.1) jekyll-theme-time-machine (= 0.1.1) - jekyll-titles-from-headings (= 0.5.1) - jemoji (= 0.9.0) - kramdown (= 1.16.2) - liquid (= 4.0.0) - listen (= 3.1.5) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.11.1) + kramdown (= 1.17.0) + liquid (= 4.0.3) mercenary (~> 0.3) - minima (= 2.4.0) - nokogiri (>= 1.8.5, < 2.0) - rouge (= 2.2.1) + minima (= 2.5.1) + nokogiri (>= 1.10.4, < 2.0) + rouge (= 3.19.0) terminal-table (~> 1.4) - github-pages-health-check (1.4.0) + github-pages-health-check (1.16.1) addressable (~> 2.3) - net-dns (~> 0.8) + dnsruby (~> 1.60) octokit (~> 4.0) - public_suffix (~> 2.0) + public_suffix (~> 3.0) typhoeus (~> 1.3) - html-pipeline (2.7.1) + html-pipeline (2.13.0) activesupport (>= 2) - nokogiri (>= 1.8.5) + nokogiri (>= 1.4) http_parser.rb (0.6.0) i18n (0.9.5) concurrent-ruby (~> 1.0) - jekyll (3.7.4) + jekyll (3.8.7) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) @@ -98,51 +99,50 @@ GEM pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) - jekyll-avatar (0.5.0) - jekyll (~> 3.0) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) jekyll-coffeescript (1.1.1) coffee-script (~> 2.2) coffee-script-source (~> 1.11.1) - jekyll-commonmark (1.2.0) + jekyll-commonmark (1.3.1) commonmarker (~> 0.14) - jekyll (>= 3.0, < 4.0) - jekyll-commonmark-ghpages (0.1.5) + jekyll (>= 3.7, < 5.0) + jekyll-commonmark-ghpages (0.1.6) commonmarker (~> 0.17.6) - jekyll-commonmark (~> 1) - rouge (~> 2) + jekyll-commonmark (~> 1.2) + rouge (>= 2.0, < 4.0) jekyll-default-layout (0.1.4) jekyll (~> 3.0) - jekyll-feed (0.9.3) - jekyll (~> 3.3) + jekyll-feed (0.13.0) + jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) - jekyll-github-metadata (2.9.4) - jekyll (~> 3.1) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) octokit (~> 4.0, != 4.4.0) - jekyll-mentions (1.3.0) - activesupport (~> 4.0) + jekyll-mentions (1.5.1) html-pipeline (~> 2.3) - jekyll (~> 3.0) - jekyll-optional-front-matter (0.3.0) - jekyll (~> 3.0) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) jekyll-paginate (1.1.0) - jekyll-readme-index (0.2.0) - jekyll (~> 3.0) - jekyll-redirect-from (0.13.0) - jekyll (~> 3.3) - jekyll-relative-links (0.5.3) - jekyll (~> 3.3) - jekyll-remote-theme (0.2.3) - jekyll (~> 3.5) - rubyzip (>= 1.2.2, < 3.0) - typhoeus (>= 0.7, < 2.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.15.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.1) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + rubyzip (>= 1.3.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) - jekyll-seo-tag (2.4.0) - jekyll (~> 3.3) - jekyll-sitemap (1.2.0) - jekyll (~> 3.3) - jekyll-swiss (0.4.0) + jekyll-seo-tag (2.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) jekyll-theme-architect (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) @@ -170,8 +170,8 @@ GEM jekyll-theme-modernist (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.5.3) - jekyll (~> 3.5) + jekyll-theme-primer (0.5.4) + jekyll (> 3.5, < 5.0) jekyll-github-metadata (~> 2.9) jekyll-seo-tag (~> 2.0) jekyll-theme-slate (0.1.1) @@ -183,62 +183,60 @@ GEM jekyll-theme-time-machine (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.5.1) - jekyll (~> 3.3) - jekyll-watch (2.0.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.9.0) - activesupport (~> 4.0, >= 4.2.9) + jemoji (0.11.1) gemoji (~> 3.0) html-pipeline (~> 2.2) - jekyll (~> 3.0) - kramdown (1.16.2) - liquid (4.0.0) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) + jekyll (>= 3.0, < 5.0) + kramdown (1.17.0) + liquid (4.0.3) + listen (3.2.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) - mini_portile2 (2.3.0) - minima (2.4.0) - jekyll (~> 3.5) + mini_portile2 (2.4.0) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.11.3) - multipart-post (2.0.0) - net-dns (0.8.0) - nokogiri (>= 1.8.5) - mini_portile2 (~> 2.3.0) - octokit (4.8.0) + minitest (5.14.1) + multipart-post (2.1.1) + nokogiri (1.10.9) + mini_portile2 (~> 2.4.0) + octokit (4.18.0) + faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) - pathutil (0.16.1) + pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (2.0.5) - rb-fsevent (0.10.3) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) - rouge (2.2.1) - ruby-enum (0.7.2) + public_suffix (3.1.1) + rb-fsevent (0.10.4) + rb-inotify (0.10.1) + ffi (~> 1.0) + rouge (3.19.0) + ruby-enum (0.8.0) i18n - ruby_dep (1.5.0) - rubyzip (1.2.2) - safe_yaml (1.0.4) - sass (3.5.6) + rubyzip (2.3.0) + safe_yaml (1.0.5) + sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.8.1) - addressable (>= 2.3.5, < 2.6) - faraday (~> 0.8, < 1.0) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) thread_safe (0.3.6) - typhoeus (1.3.0) + typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.5) + tzinfo (1.2.7) thread_safe (~> 0.1) - unicode-display_width (1.3.0) + unicode-display_width (1.7.0) + zeitwerk (2.3.0) PLATFORMS ruby @@ -248,4 +246,4 @@ DEPENDENCIES jekyll-redirect-from BUNDLED WITH - 1.16.1 + 2.1.4 diff --git a/docs/_config.yaml b/docs/_config.yaml index d753141e..54752e15 100644 --- a/docs/_config.yaml +++ b/docs/_config.yaml @@ -1,5 +1,8 @@ theme: jekyll-theme-minimal -exclude: [ruby-bundle] +exclude: [ruby-bundle, vendor] plugins: - jekyll-redirect-from google_analytics: UA-93396600-3 +collections: + api: + output: true diff --git a/docs/_data/index.yaml b/docs/_data/index.yaml index bd258a11..0ff47762 100644 --- a/docs/_data/index.yaml +++ b/docs/_data/index.yaml @@ -3,32 +3,39 @@ # https://jekyllrb.com/tutorials/navigation/#scenario-8-retrieving-items-based-on-front-matter-properties sections: - - title: For Users + - title: General docs: - title: Getting Started url: / - title: ODrive Tool - url: odrivetool + url: /odrivetool - title: Parameters & Commands - url: commands + url: /commands - title: Interfaces - url: interfaces + url: /interfaces - title: Encoders - url: encoders + url: /encoders + - title: Homing & Endstops + url: /endstops + - title: Thermistors + url: /thermistors - title: Control & Tuning - url: control - - title: Hoverboard Guide - url: hoverboard + url: /control - title: Troubleshooting - url: troubleshooting + url: /troubleshooting + - title: Tutorials + docs: + - title: Hoverboard Guide + url: /hoverboard + - title: API Reference - title: For ODrive Developers docs: - title: Firmware Developer Guide - url: developer-guide + url: /developer-guide - title: Configuring Visual Studio Code - url: configuring-vscode + url: /configuring-vscode - title: Configuring Eclipse - url: configuring-eclipse + url: /configuring-eclipse - title: Component Guides docs: - title: Motor Guide diff --git a/docs/_layouts/api_documentation_template.j2 b/docs/_layouts/api_documentation_template.j2 new file mode 100644 index 00000000..53fd127a --- /dev/null +++ b/docs/_layouts/api_documentation_template.j2 @@ -0,0 +1,148 @@ +--- +title: '[% if interface %][[interface.fullname]][% else %][[enum.fullname]][% endif %]' +layout: default +edit_url: 'Firmware/odrive-interface.yaml' +download: + url: 'Firmware/odrive-interface.yaml' + text: 'download as YAML' +--- + +[%- macro interface_ref(type) -%] +**[['[']][[type.name]][[']']]([[type.fullname | lower]])** +[%- endmacro %] + +[%- macro value_type_ref(type) -%] +[%- if type.builtin -%] +[[type.name]] +[%- else -%] +[['[']][[type.name]][[']']]([[type.fullname | lower]]) +[%- endif %] +[%- endmacro %] + +[% macro attr_ref(token, attr) -%] +**[['[']][[token]][[']']]([[attr.parent.fullname | lower]]#[[attr.name]])** +[%- endmacro %] + +[% if interface %] +[% set scope = interface %] +[% else %] +[% set scope = enum.parent %] +[% endif %] + +[%- macro doc_tokenize(text) %][[ text | tokenize(scope, interface_ref, value_type_ref, attr_ref) ]][% endmacro %] + +[%- macro status_badge(status) %] +[%- if status == 'experimental' %] +Experimental +[%- endif %] +[%- if status == 'deprecated' %] +Deprecated +[%- endif %] +[%- endmacro %] + +[%- macro breadcrumbs(title) %] +# [% for item in title.split('.') | diagonalize -%] +[[item[-1]]] +[%- if not loop.last %] 〉[% endif %] +[%- endfor %] +[%- endmacro %] + +[% if interface %] + +[[breadcrumbs(interface.fullname)]] + +[%- if interface.doc or interface.brief %] +[[doc_tokenize(interface.brief)]][% if interface.brief and interface.doc %] + +[% endif %][[doc_tokenize(interface.doc)]] +[%- endif %] + +## Attributes + +[% if interface.attributes %] +[% for attr in interface.attributes.values() %] +[%- if attr.type.purename == 'fibre.Property' %] +**[[attr.name]]**  —  [[value_type_ref(attr.type.value_type)]]    _[[attr.type.mode]]_ +[%- else %] +**[[attr.name]]**  —  [[interface_ref(attr.type)]] +[%- endif %] +[[-status_badge(attr.status)]] + +
    +[% if attr.doc or attr.brief %] +[[doc_tokenize(attr.brief)]][% if attr.brief and attr.doc %] + +[% endif %][%- if attr.unit %] + +**Unit:** [[attr.unit]] + +[% endif %][[doc_tokenize(attr.doc)]] +[%- else %] +_No description_ +[%- endif %] +
+[% endfor %] +[% else %] +This interface has no attributes. +[% endif %] + +## Functions + +[% if interface.functions %] +[% for function in interface.functions.values() %] +**[[function.name]]**([% for arg in function.in.values() | skip_first %][[arg.name]]: [[value_type_ref(arg.type)]][[', ' if not loop.last]][% endfor %])[% if function.out %]  ➔  [% for arg in function.out.values() %][[arg.name]]: [[value_type_ref(arg.type)]][[', ' if not loop.last]][% endfor %][% endif %] + +
    +[% if function.doc or function.brief %] +[[doc_tokenize(function.brief)]][% if function.brief and function.doc %] + +[% endif %][[doc_tokenize(function.doc)]] +[%- else %] +_No description_ +[%- endif %] +[% if function.in.values() | skip_first %] +**Inputs:** +[%- for arg in function.in.values() | skip_first %] + - `[[arg.name]]`: [% if arg.doc %][[doc_tokenize(arg.doc)]][% else %] _No description_[% endif %] +[%- endfor %] +[%- endif %] +[% if function.out.values() %] +**Outputs:** +[%- for arg in function.out.values() %] + - `[[arg.name]]`: [% if arg.doc %][[doc_tokenize(arg.doc)]][% else %] _No description_[% endif %] +[%- endfor %] +[%- endif %] +
+[% endfor %] +[% else %] +This interface has no functions. +[% endif %] + +[% else %] + +[[breadcrumbs(enum.fullname)]] + +[%- if enum.doc or enum.brief %] +[[doc_tokenize(enum.brief)]][% if enum.brief and enum.doc %] + +[% endif %][[doc_tokenize(enum.doc)]] +[%- endif %] + +## [% if enum.is_flags %]Flags[% else %]Values[% endif %] + +[% for k, value in enum['values'].items() %] +**[[(enum.name + value.name) | to_macro_case]]**  —  [% if enum.is_flags %]0x[['%08x' | format(value.value)]][% else %][[value.value]][% endif %] +[[-status_badge(value.status)]] + +
    +[% if value.doc or value.brief %] +[[doc_tokenize(value.brief)]][% if value.brief and value.doc %] + +[% endif %][[doc_tokenize(value.doc)]] +[%- else %] +_No description_ +[%- endif %] +
+[% endfor %] + +[% endif %] diff --git a/docs/_layouts/api_index_template.j2 b/docs/_layouts/api_index_template.j2 new file mode 100644 index 00000000..087fbced --- /dev/null +++ b/docs/_layouts/api_index_template.j2 @@ -0,0 +1,41 @@ +[%- macro dump_interfaces(interfaces, level) %] +[%- for intf in interfaces %] +[%- if intf.interfaces or intf.value_types %] +
  • +{% assign myvar = (page.title + '.') | split: "[[intf.fullname + '.']]" %} + + +
      +[[dump_interfaces(intf.interfaces, level + 1) | indent(4)]] +[[dump_value_types(intf.enums, level + 1) | indent(4)]] +
    +
  • +[%- else %] +
  • + +
  • +[%- endif %] +[%- endfor %] +[%- endmacro %] + +[%- macro dump_value_types(value_types, level) %] +[%- for enum in value_types %] +
  • + +
  • +[%- endfor %] +[%- endmacro %] + +[[dump_interfaces(toplevel_interfaces, 0)]] diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 9be4b36e..5569b3a5 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -1,3 +1,6 @@ +{% assign pagename = page.url | replace_first: '/', '' | replace: '.html', '' %} +{% if pagename == '' %}{% assign pagename = 'getting-started' %}{% endif %} + @@ -23,20 +26,29 @@ {% endif %}

    {{ site.description | default: site.github.project_tagline }}

    - + +
    - +
    +
    {% if site.github.is_project_page %}

    View the Project on GitHub {{ site.github.repository_nwo }}

    {% endif %} @@ -59,15 +71,30 @@
    -
    - {% assign filename = page.url | replace_first: '/', '' | replace: '.html', '.md' %} - {% if filename == '' %}{% assign filename = 'getting-started.md' %}{% endif %} - - - edit on GitHub -
    +
    +
    + + + {% if page.edit_url %} + {% assign edit_url = "https://www.github.com/madcowswe/ODrive/edit/master/" | append: edit_url %} + {% else %} + {% assign edit_url = "https://www.github.com/madcowswe/ODrive/edit/master/docs/" | append: pagename | append: ".md" %} + {% endif %} + edit on GitHub +
    + + {% if page.download %} + + {% endif %} +
    + {{ content }} @@ -105,6 +132,11 @@ } } + diff --git a/docs/ascii-protocol.md b/docs/ascii-protocol.md index a0757e2f..9625ec47 100644 --- a/docs/ascii-protocol.md +++ b/docs/ascii-protocol.md @@ -5,11 +5,13 @@ * **Via USB:** * **Windows:** Use the Zadig utility to set the ODrive's driver to "usbser". Windows will then make the device available as COM port. You can use [PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/) to manually send commands or open the COM port using your favorite programming language - * **Linux/macOS:** Run `/dev/tty*` to list all serial ports. The ODrive will show up as `/dev/ttyACM0` on Linux and `/dev/tty.usbmodem[...]` on macOS. Once you know the name, you can use `screen /dev/ttyACM0` (with the correct name) to send commands manually or open the device using your favorite programming language. Serial ports on Unix can be opened, written to and read from like a normal file. + * **Linux/macOS:** Run `/dev/tty*` to list all serial ports. The ODrive will show up as `/dev/ttyACM0` (or similar) on Linux and `/dev/tty.usbmodem[...]` on macOS. Once you know the name, you can use `screen /dev/ttyACM0` (with the correct name) to send commands manually or open the device using your favorite programming language. Serial ports on Unix can be opened, written to and read from like a normal file. * **Via UART:** Connect the ODrive's TX (GPIO1) to your host's RX. Connect your ODrive's RX (GPIO2) to your host's TX. The logic level of the ODrive is 3.3V. * **Arduino:** You can use the [ODrive Arduino library](https://github.com/madcowswe/ODrive/tree/master/Arduino/ODriveArduino) to talk to the ODrive. * **Windows/Linux/macOS:** You can use an FTDI USB-UART cable to connect to the ODrive. +The ODrive does not echo commands. That means that when you type commands into a program like `screen`, the characters you type won't show up in the console. + ## Command format The ASCII protocol is human-readable and line-oriented, with each line having the following format: @@ -18,7 +20,7 @@ The ASCII protocol is human-readable and line-oriented, with each line having th command *42 ; comment [new line character] ``` - * `*42` stands for a GCode compatible checksum and can be omitted. If and only if a checksum is provided, the device will also include a checksum in the response, if any. + * `*42` stands for a GCode compatible checksum and can be omitted. If and only if a checksum is provided, the device will also include a checksum in the response, if any. If the checksum is provided but is not valid, the line is ignored. The checksum is calculated as the bitwise xor of all characters before the asterisk (`*`).
    Example of a valid checksum: `r vbus_voltage *93`. * comments are supported for GCode compatibility * the command is interpreted once the new-line character is encountered @@ -30,9 +32,9 @@ t motor destination ``` * `t` for trajectory * `motor` is the motor number, `0` or `1`. -* `destination` is the goal position, in encoder counts. +* `destination` is the goal position, in [turns]. -Example: `t 0 -20000` +Example: `t 0 -2` For general moving around of the axis, this is the recommended command. @@ -43,26 +45,26 @@ For basic use where you send one setpoint at at a time, use the `q` command. If you have a realtime controller that is streaming setpoints and tracking a trajectory, use the `p` command. ``` -q motor position velocity_lim current_lim +q motor position velocity_lim torque_lim ``` * `q` for position * `motor` is the motor number, `0` or `1`. -* `position` is the desired position, in encoder counts. -* `velocity_lim` is the velocity limit, in counts/s (optional). -* `current_lim` is the current limit, in A (optional). +* `position` is the desired position, in [turns]. +* `velocity_lim` is the velocity limit, in [turns/s] (optional). +* `torque_lim` is the torque limit, in [Nm] (optional). -Example: `q 0 -20000 10000 10` +Example: `q 0 -2 1 0.1` ``` -p motor position velocity_ff current_ff +p motor position velocity_ff torque_ff ``` * `p` for position * `motor` is the motor number, `0` or `1`. -* `position` is the desired position, in encoder counts. -* `velocity_ff` is the velocity feed-forward term, in counts/s (optional). -* `current_ff` is the current feed-forward term, in A (optional). +* `position` is the desired position, in [turns]. +* `velocity_ff` is the velocity feed-forward term, in [turns/s] (optional). +* `torque_ff` is the torque feed-forward term, in [Nm] (optional). -Example: `p 0 -20000 0 0` +Example: `p 0 -2 0 0` Note that if you don't know what feed-forward is or what it's used for, simply omit it. @@ -70,14 +72,14 @@ This command updates the watchdog timer for the motor. #### Motor Velocity command ``` -v motor velocity current_ff +v motor velocity torque_ff ``` * `v` for velocity * `motor` is the motor number, `0` or `1`. -* `velocity` is the desired velocity in counts/s. -* `current_ff` is the current feed-forward term, in A (optional). +* `velocity` is the desired velocity in [turns/s]. +* `torque_ff` is the torque feed-forward term, in [Nm] (optional). -Example: `v 0 1000 0` +Example: `v 0 1 0` Note that if you don't know what feed-forward is or what it's used for, simply omit it. @@ -85,11 +87,11 @@ This command updates the watchdog timer for the motor. #### Motor Current command ``` -c motor current +c motor torque ``` -* `c` for current +* `c` for torque * `motor` is the motor number, `0` or `1`. -* `current` is the desired current in A. +* `torque` is the desired torque in [Nm]. This command updates the watchdog timer for the motor. @@ -101,8 +103,8 @@ response: pos vel ``` * `f` for feedback -* `pos` is the encoder position in counts (float) -* `vel` is the encoder velocity in counts/s (float) +* `pos` is the encoder position in [turns] (float) +* `vel` is the encoder velocity in [turns/s] (float) #### Update motor watchdog ``` @@ -124,14 +126,14 @@ Not all parameters can be accessed via the ASCII protocol but at least all param ``` * `property` name of the property, as seen in ODrive Tool * response: text representation of the requested value - * Example: `r vbus_voltage` => response: `24.087744` + * Example: `r vbus_voltage` => response: `24.087744` <new line> * Writing: ``` w [property] [value] ``` * `property` name of the property, as seen in ODrive Tool * `value` text representation of the value to be written - * Example: `w axis0.controller.pos_setpoint -123.456` + * Example: `w axis0.controller.input_pos -123.456` #### System commands: * `ss` - Save config diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index 1ddd4fb8..92bf03ca 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -100,7 +100,7 @@ table { width:100%; border-collapse:collapse; display: block; - overflow-x: scroll; + overflow-x: auto; } th, td { @@ -128,10 +128,12 @@ header { float:left; position:fixed; -webkit-font-smoothing:subpixel-antialiased; - - overflow-y: auto; - top: 50px; + height: 100%; + display: flex; + flex-direction: column; + top: 0; bottom: 0; + padding: 10px 0; } @@ -261,7 +263,11 @@ a { } h1 a { - color: unset; + color: unset; +} + +.navitem a { + color: unset; } // a:hover, a:focus { @@ -270,6 +276,7 @@ h1 a { // } /*** Navigation bar ***/ + header > div { margin-right: 20px; } @@ -287,42 +294,68 @@ header li { #navbar { max-width: 250px; + flex: 1; + overflow: auto; + margin: 0; } -header ul p { - margin:0; +.navgroup { + background: #cbcbcb; + margin-top: 20px; +} +.navgroup:first-child { + margin-top: 0px; +} + +#navbar ul { + background-color: rgba(255, 255, 255, 0.87); + margin: 0; +} + +.navheader { + margin:0px; padding-left:5px; display: block; // color: #d60000; color: #000; font-weight: bold; - background-color: #cbcbcb; } -header ul ul li a { - background: #f8f8f8; +.navitem { //border:1px solid #e0e0e0; - line-height:1; font-size:12px; font-weight: bold; color:#676767; - display:block; + display:flex; text-align:left; - padding:12px 0px 5px 5px; - //margin:12px; - height:20px; + padding:0px 5px; + margin:0px; + height:37px; + line-height:37px; } -//// rounded edges (look bad) -//header ul p { -// border-radius:5px 5px 0 0; -//} -//header ul ul li:last-child a { -// border-radius:0 0 5px 5px; -//} +.navitem a { + display: block; + width: 100%; +} + +.levelbar { + float: inline-start; + margin-left: 8px; + margin-right: 5px; + border-left: 1px solid rgba(0, 0, 0, 0.3); + width: 1px !important; +} + +.currentitem { + //-webkit-box-shadow: inset 0px 0px 5px 3px #aa0000a6; + //-moz-box-shadow: inset 0px 0px 5px 3px #aa0000a6; + //box-shadow: inset 0px 0px 5px 3px #aa0000a6; + color: #d60000; +} /*** Navbar Hover ***/ -header ul a:hover, header ul a:focus { +.navitem:hover, .navitem:focus { color: #d60000; // color:rgb(0, 0, 0); // background-color: rgba(0, 0, 0, 0.24); @@ -418,11 +451,14 @@ details > div > p:last-child { border-left-color: #5bc0de; } -/*** edit link ***/ -.edit { +/*** edit/download link ***/ +.pageactions { float: right; font-size: 12px; } +.pageactions > div { + text-align: right; +} /*** inline code ***/ :not(pre) > code { @@ -441,3 +477,38 @@ table th { table tr:nth-child(2n) { background-color: #f8f8f8; } + + +.expandable-list { + height: 100%; + margin: 0px; + //background-color: #ffbfbf61; + max-height: 0; + overflow: hidden; + -webkit-transition: max-height .5s ease-in-out; + transition: max-height .5s ease-in-out; +} + +#navbar input[type=checkbox]:checked ~ .expandable-list { /* reset the height when checkbox is checked */ + max-height: 1000px; +} + +.chevron:before { + text-align: left; + content: "\3009" +} + +.chevron { + float: left; + -webkit-transition: -webkit-transform .5s ease; + transition: transform .5s ease; + transform-origin: 40% 50%; + padding-left: 5px; + padding-right: 5px; +} + +#navbar input[type=checkbox]:checked ~ p .chevron { /* rotate down when checkbox is checked */ + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} diff --git a/docs/can-protocol.md b/docs/can-protocol.md new file mode 100644 index 00000000..0b3f0c84 --- /dev/null +++ b/docs/can-protocol.md @@ -0,0 +1,86 @@ +# CAN Protocol + +## Hardware Setup +ODrive assumes the CAN PHY is a standard differential twisted pair in a linear bus configuration with 120 ohm termination resistance at each end. ODrive versions less than V3.5 include a soldered 120 ohm termination resistor, but ODrive versions V3.5 and greater implement a dip switch to toggle the termination. ODrive uses 3.3v as the high output, but conforms to the CAN PHY requirement of achieving a differential voltage > 1.5V to represent a "0". As such, it is compatible with standard 5V bus architectures. + +ODrive currently supports the following CAN baud rates: +* 125 kbps +* 250 kbps (default) +* 500 kbps +* 1000 kbps + +--- +## Transport Protocol +We've implemented a very basic CAN protocol that we call "CAN Simple" to get users going with ODrive. This protocol is sufficiently abstracted that it is straightforward to add other protocols such as CANOpen, J1939, or Fibre over ISO-TP in the future. Unfortunately, implementing those protocols is a lot of work, and we wanted to give users a way to control ODrive's basic functions via CAN sooner rather than later. + +### CAN Frame +At its most basic, the CAN Simple frame looks like this: + +* Upper 6 bits - Node ID - max 0x3F (or 0xFFFFFF when using extended CAN IDs) +* Lower 5 bits - Command ID - max 0x1F + +To understand how the Node ID and Command ID interact, let's look at an example + +`odrv0.axis0.can_node_id = 0x010` - Reserves messages 0x200 through 0x21F +`odrv0.axis1.can_node_id = 0x018` - Reserves messages 0x300 through 0x31F + +It may not be obvious, but this allows for some compatibility with CANOpen. Although the address space 0x200 and 0x300 correspond to receive PDO base addresses, we can guarantee they will not conflict if all CANopen node IDs are >= 32. E.g.: + +CANopen nodeID = 35 = 0x23 +Receive PDO 0x200 + nodeID = 0x223, which does not conflict with the range [0x200 : 0x21F] + +Be careful that you don't assign too many nodeIDs per PDO group. Four CAN Simple nodes (32*4) is all of the available address space of a single PDO. If the bus is strictly ODrive CAN Simple nodes, a simple sequential Node ID assignment will work fine. + +### Messages + +CMD ID | Name | Sender | Signals | Start byte | Signal Type | Bits | Factor | Offset | Byte Order +--: | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- +0x000 | CANOpen NMT Message\*\* | Master | - | - | - | - | - | - | - +0x001 | ODrive Heartbeat Message | Axis | Axis Error
    Axis Current State | 0
    4 | Unsigned Int
    Unsigned Int | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x002 | ODrive Estop Message | Master | - | - | - | - | - | - | - +0x003 | Get Motor Error\* | Axis | Motor Error | 0 | Unsigned Int | 32 | 1 | 0 | Intel +0x004 | Get Encoder Error\* | Axis | Encoder Error | 0 | Unsigned Int | 32 | 1 | 0 | Intel +0x005 | Get Sensorless Error\* | Axis | Sensorless Error | 0 | Unsigned Int | 32 | 1 | 0 | Intel +0x006 | Set Axis Node ID | Master | Axis CAN Node ID | 0 | Unsigned Int | 32 | 1 | 0 | Intel +0x007 | Set Axis Requested State | Master | Axis Requested State | 0 | Unsigned Int | 32 | 1 | 0 | Intel +0x008 | Set Axis Startup Config | Master | - Not yet implemented - | - | - | - | - | - | - +0x009 | Get Encoder Estimates\* | Master | Encoder Pos Estimate
    Encoder Vel Estimate | 0
    4 | IEEE 754 Float
    IEEE 754 Float | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x00A | Get Encoder Count\* | Master | Encoder Shadow Count
    Encoder Count in CPR | 0
    4 | Signed Int
    Signed Int | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x00B | Set Controller Modes | Master | Control Mode
    Input Mode | 0
    4 | Signed Int
    Signed Int | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x00C | Set Input Pos | Master | Input Pos
    Vel FF
    Torque FF | 0
    4
    6 | IEEE 754 Float
    Signed Int
    Signed Int | 32
    16
    16 | 1
    0.001
    0.001 | 0
    0
    0 | Intel
    Intel
    Intel +0x00D | Set Input Vel | Master | Input Vel
    Torque FF | 0
    4 | IEEE 754 Float
    IEEE 754 Float | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x00E | Set Input Torque | Master | Input Torque | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel +0x00F | Set Velocity Limit | Master | Velocity Limit | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel +0x010 | Start Anticogging | Master | - | - | - | - | - | - | - +0x011 | Set Traj Vel Limit | Master | Traj Vel Limit | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel +0x012 | Set Traj Accel Limits | Master | Traj Accel Limit
    Traj Decel Limit | 0
    4 | IEEE 754 Float
    IEEE 754 Float | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x013 | Set Traj Inertia | Master | Traj Inertia | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel +0x014 | Get IQ\* | Axis | Iq Setpoint
    Iq Measured | 0
    4 | IEEE 754 Float
    IEEE 754 Float | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x015 | Get Sensorless Estimates\* | Master | Sensorless Pos Estimate
    Sensorless Vel Estimate | 0
    4 | IEEE 754 Float
    IEEE 754 Float | 32
    32 | 1
    1 | 0
    0 | Intel
    Intel +0x016 | Reboot ODrive | Master\*\*\* | - | - | - | - | - | - | - +0x017 | Get Vbus Voltage | Master\*\*\* | Vbus Voltage | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel +0x018 | Clear Errors | Master | - | - | - | - | - | - | - +0x700 | CANOpen Heartbeat Message\*\* | Slave | - | - | - | - | - | - | - +-|-|-|----------------------------------|-|--------------------|-|-|-|_ + +\* Note: These messages are call & response. The Master node sends a message with the RTR bit set, and the axis responds with the same ID and specified payload. +\*\* Note: These CANOpen messages are reserved to avoid bus collisions with CANOpen devices. They are not used by CAN Simple. +\*\*\* Note: These messages can be sent to either address on a given ODrive board. + +--- +## Configuring ODrive for CAN +Configuration of the CAN parameters should be done via USB before putting the device on the bus. + +To set the desired baud rate, use `.can.set_baud_rate()`. The baud rate can be done without rebooting the device. If you'd like to keep the baud rate, simply call `.save_configuration()` before rebooting. + +Each axis looks like a separate node on the bus. Thus, they both have the two properties `can_node_id` and `can_node_id_extended`. The node ID can be from 0 to 63 (0x3F) inclusive, or, if extended CAN IDs are used, from 0 to 16777215 (0xFFFFFF). + +### Example Configuration + +``` +odrv0.axis0.config.can_node_id = 3 +odrv0.axis1.config.can_node_id = 1 +odrv0.can.set_baud_rate(500000) +odrv0.save_configuration() +odrv0.reboot() +``` diff --git a/docs/commands.md b/docs/commands.md index 1dce7336..1b41b30f 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -18,24 +18,8 @@ For the most part, both axes on the ODrive can be controlled independently. ### State Machine -The current state of an axis is indicated by `.current_state`. The user can request a new state by assigning a new value to `.requested_state`. The default state after startup is `AXIS_STATE_IDLE`. +The current state of an axis is indicated by [`.current_state`](api/odrive.axis#current_state). The user can request a new state by assigning a new value to [`.requested_state`](api/odrive.axis#current_state). The default state after startup is `AXIS_STATE_IDLE`. A description of all states can be found [here](api/odrive.axis.axisstate). - 1. `AXIS_STATE_IDLE` Disable motor PWM and do nothing. - 2. `AXIS_STATE_STARTUP_SEQUENCE` Run the [startup procedure](#startup-procedure). - 3. `AXIS_STATE_FULL_CALIBRATION_SEQUENCE` Run motor calibration and then encoder offset calibration (or encoder index search if `.encoder.config.use_index` is `True`). - 4. `AXIS_STATE_MOTOR_CALIBRATION` Measure phase resistance and phase inductance of the motor. - * To store the results set `.motor.config.pre_calibrated` to `True` and [save the configuration](#saving-the-configuration). After that you don't have to run the motor calibration on the next start up. - * This modifies the variables `.motor.config.phase_resistance` and `.motor.config.phase_inductance`. - 5. `AXIS_STATE_SENSORLESS_CONTROL` Run sensorless control. - * The motor must be calibrated (`.motor.is_calibrated`) - * [`.controller.control_mode`](#control-mode) must be `True`. - 6. `AXIS_STATE_ENCODER_INDEX_SEARCH` Turn the motor in one direction until the encoder index is traversed. This state can only be entered if `.encoder.config.use_index` is `True`. - 7. `AXIS_STATE_ENCODER_OFFSET_CALIBRATION` Turn the motor in one direction for a few seconds and then back to measure the offset between the encoder position and the electrical phase. - * Can only be entered if the motor is calibrated (`.motor.is_calibrated`). - * A successful encoder calibration will make the `.encoder.is_ready` go to true. - 8. `AXIS_STATE_CLOSED_LOOP_CONTROL` Run closed loop control. - * The action depends on the [control mode](#control-mode). - * Can only be entered if the motor is calibrated (`.motor.is_calibrated`) and the encoder is ready (`.encoder.is_ready`). ### Startup Procedure @@ -49,27 +33,30 @@ The ODrive will sequence all enabled startup actions selected in the order shown * `.config.startup_closed_loop_control` * `.config.startup_sensorless_control` -See [state machine](#state-machine) for a description of each state. +See [here](api/odrive.axis.axisstate) for a description of each state. ### Control Mode The default control mode is position control. If you want a different mode, you can change `.controller.config.control_mode`. -Possible values are: -* `CTRL_MODE_POSITION_CONTROL` -* `CTRL_MODE_VELOCITY_CONTROL` -* `CTRL_MODE_CURRENT_CONTROL` -* `CTRL_MODE_VOLTAGE_CONTROL` - this one is not normally used. +Possible values are listed [here](api/odrive.axis.controller.controlmode). -# Control Commands -* `.controller.pos_setpoint = ` -* `.controller.vel_setpoint = ` -* `.controller.current_setpoint = ` +### Input Mode + +As of version v0.5.0, ODrive now intercepts the incoming commands and can apply filters to them. The old protocol values `pos_setpoint`, `vel_setpoint`, and `current_setpoint` are still used internally by the closed-loop cascade control, but the user cannot write to them directly. This allows us to condense the number of ways the ODrive accepts motion commands. The new commands are: + +* `.controller.input_pos = ` +* `.controller.input_vel = ` +* `.controller.input_torque = ` + +Modes can be selected by changing `.controller.config.input_mode`. +The default input mode is `INPUT_MODE_PASSTHROUGH`. +Possible values are listed [here](api/odrive.axis.controller.inputmode). ## System monitoring commands ### Encoder position and velocity -* View encoder position with `.encoder.pos_estimate` [counts] -* View rotational velocity with `.encoder.vel_estimate` [counts/s] +* View encoder position with `.encoder.pos_estimate` [turns] or `.encoder.pos_est_counts` [counts] +* View rotational velocity with `.encoder.vel_estimate` [turn/s] or `.encoder.vel_est_counts` [count/s] ### Motor current and torque estimation * View the commanded motor current with `.motor.current_control.Iq_setpoint` [A] @@ -84,7 +71,7 @@ Using the motor current and the known KV of your motor you can estimate the moto All variables that are part of a `[...].config` object can be saved to non-volatile memory on the ODrive so they persist after you remove power. The relevant commands are: * `.save_configuration()`: Stores the configuration to persistent memory on the ODrive. - * `.erase_configuration()`: Resets the configuration variables to their factory defaults. This only has an effect after a reboot. A side effect of this command is that motor control stops (in case it was running) and the USB communication breaks out temporarily. This is because erasing flash pages hangs the microcontroller for several seconds. + * `.erase_configuration()`: Resets the configuration variables to their factory defaults. This also reboots the device. ### Diagnostics @@ -94,9 +81,8 @@ All variables that are part of a `[...].config` object can be saved to non-volat ## Setting up sensorless The ODrive can run without encoder/hall feedback, but there is a minimum speed, usually around a few hunderd RPM. -However the units of this mode is different from when using an encoder. Velocities are not measured in counts/s, instead it is electrical rad/s. This also applies to the gains. For example, `vel_gain` is in units of `A / (rad/s)` instead of `A / (count/s)`. -To give an example, suppose you have a motor with 7 pole pairs, and you want to spin it at 3000 RPM. Then you would set the `vel_setpoint` to `3000 * 2*pi/60 * 7 = 2199 rad/s electrical`. +To give an example, suppose you have a motor with 7 pole pairs, and you want to spin it at 3000 RPM. Then you would set the `input_vel` to `3000 * 2*pi/60 * 7 = 2199 rad/s electrical`. Below are some suggested starting parameters that you can use. Note that you _must_ set the `pm_flux_linkage` correctly for sensorless mode to work. @@ -104,7 +90,7 @@ Below are some suggested starting parameters that you can use. Note that you _mu odrv0.axis0.controller.config.vel_gain = 0.01 odrv0.axis0.controller.config.vel_integrator_gain = 0.05 odrv0.axis0.controller.config.control_mode = 2 -odrv0.axis0.controller.vel_setpoint = 400 +odrv0.axis0.controller.input_vel = 400 odrv0.axis0.motor.config.direction = 1 odrv0.axis0.sensorless_estimator.config.pm_flux_linkage = 5.51328895422 / ( * ) ``` diff --git a/docs/configuring-vscode.md b/docs/configuring-vscode.md index 572f9933..0f08da1e 100644 --- a/docs/configuring-vscode.md +++ b/docs/configuring-vscode.md @@ -10,25 +10,26 @@ Before doing the VSCode setup, make sure you've installed all of your [prerequis 1. Open VSCode 1. Install extensions. This can be done directly from VSCode (Ctrl+Shift+X) * Required extensions: - * C/C++ - * Cortex-Debug + * C/C++ `ext install ms-vscode.cpptools` + * Cortex-Debug `ext install marus25.cortex-debug` + * Cortex-Debug: Device Support Pack - STM32F4 `ext install marus25.cortex-debug-dp-stm32f4` * Recommended Extensions: * Include Autocomplete * Path Autocomplete * Auto Comment Blocks -1. Create an environment variable named `ARM_GCC_ROOT` whose value is the location of the `GNU Arm Embedded Toolchain` (.e.g `C:\Program Files (x86)\GNU Tools Arm Embedded\7 2018-q2-update`) that you installed in the prerequisites section of the developer's guide. -1. Restart VSCode +1. Create an environment variable named `ARM_GCC_ROOT` whose value is the location of the `GNU Arm Embedded Toolchain` (.e.g `C:\Program Files (x86)\GNU Tools Arm Embedded\7 2018-q2-update`) that you installed in the prerequisites section of the developer's guide. This is not strictly needed for Linux or Mac, and you can alternatively use the `Cortex-debug: Arm Toolchain Path` setting in VSCode extension settings. +1. Relaunch VSCode 1. Open the VSCode Workspace file, which is located in the root of the ODrive repository. It is called `ODrive_Workspace.code-workspace`. The first time you open it, VSCode will install some dependencies. If it fails, you may need to [change your proxy settings](https://code.visualstudio.com/docs/getstarted/settings). You should now be ready to compile and test the ODrive project. ## Building the Firmware -* Tasks -> Run Build Task +* Terminal -> Run Build Task (Ctrl+Shift+B) A terminal window will open with your native shell. VSCode is configured to run the command `make -j4` in this terminal. ## Flashing the Firmware -* Tasks -> Run Task -> flash +* Terminal -> Run Task -> flash A terminal window will open with your native shell. VSCode is configured to run the command `make flash` in this terminal. @@ -40,13 +41,15 @@ An extension called Cortex-Debug has recently been released which is designed sp Note: If developing on Windows, you should have `arm-none-eabi-gdb` and `openOCD` on your PATH. * Make sure you have the Firmware folder as your active folder + * Set `CONFIG_DEBUG=true` in the tup.config file * Flash the board with the newest code (starting debug session doesn't do this) - * Debug -> Start Debugging (or press F5) + * In the _Run_ tab (Ctrl+Shift+D), select "Debug ODrive (Firmware)" + * Press _Start Debugging_ (or press F5) * The processor will reset and halt. * Set your breakpoints. Note: you can only set breakpoints when the processor is halted, if you set them during run mode, they won't get applied. - * Run (F5) + * _Continue_ (F5) * Stepping over/in/out, restarting, and changing breakpoints can be done by first pressing the "pause" (F6) button at the top the screen. - * When done debugging, simply stop (Shift+F5) the debugger. It will kill your openOCD process too. + * When done debugging, simply stop (Shift+F5) the debugger. It will kill your openOCD process too. ## Cleaning the Build This sometimes needs to be done if you change branches. diff --git a/docs/control.md b/docs/control.md index 8af90a91..0f00d05f 100644 --- a/docs/control.md +++ b/docs/control.md @@ -1,8 +1,8 @@ # Control -The motor controller is a cascaded style position, velocity and current control loop, as per the diagram below. When the control mode is set to position control, the whole loop runs. When running in velocity control mode, the position control part is removed and the velocity command is fed directly in to the second stage input. In current control mode, only the current controller is used. +The motor controller is a cascaded style position, velocity and current control loop, as per the diagram below. When the control mode is set to position control, the whole loop runs. When running in velocity control mode, the position control part is removed and the velocity command is fed directly in to the second stage input. In torque control mode, only the current controller is used. -![Cascaded pos vel I loops](https://github.com/madcowswe/ODrive/blob/master/docs/controller_with_ff.png?raw=true) +![Cascaded pos vel I loops](controller_with_ff.png) Each stage of the control loop is a variation on a [PID controller](https://en.wikipedia.org/wiki/PID_controller). A PID controller is a mathematical model that can be adapted to control a wide variety of systems. This flexibility is essential as it allows the ODrive to be used to control all kinds of mechanical systems. @@ -30,11 +30,19 @@ voltage_cmd = current_error * current_gain + voltage_integral (+ voltage_feedfor ``` For more detail refer to [controller.cpp](https://github.com/madcowswe/ODrive/blob/master/Firmware/MotorControl/controller.cpp#L86). + +### Controller Details: +The ultimate output of the controller is the voltage applied to the gate of each FET to deliver current through each coil of the motor. The current through the motor linearly relates to the torque output of the motor. This means that the inputs to the cascaded controller are theoretically the position (angle), velocity (angle/time), and acceleration (angle/time/time) of the motor. Note that when thinking about the controller from the perpective of the physics of the motor you would expect to see the time in the Velocity and Current loops, but it is absent because the time difference between iterations is always 125 microseconds (8kHz). Because the time difference between controller loops is a constant and can simply be wrapped into the controller gains. + +The output of each stage of the controller is clamped before being fed into the next stage. So after the `vel_cmd` is calculated from the position controller, the `vel_cmd` is clamped to the velocity limit. The `torque_cmd` output of the velocity controller is then clamped and fed to the current controller. Oddly enough the controller class does not contain the current controller, but instead the current controller is housed in the motor class due to the complexity of the motor driver schema. + +The feedforward terms available when using the position or velocity control mode are meant to enable better performance when the dynamics of a system are known and the host controller can predict the motion based on the load. A perfect example of this is the use of the trajectory controller that sets the position, velocity, and torque based on the desired position, velocity, and acceleration. If you take a trapezoidal velocity profile for example, you can imagine on the ramp upward the velocity will be increasing over time, while the torque is a non-zero constant. At the flat portion of the profile the velocity will be a non-zero constant, but the acceleration will be zero. This trajectory controller use case uses the cascaded controller with multiple inputs to achieve the desired motion with the best performance. + ## Tuning Tuning the motor controller is an essential step to unlock the full potential of the ODrive. Tuning allows for the controller to quickly respond to disturbances or changes in the system (such as an external force being applied or a change in the setpoint) without becoming unstable. Correctly setting the three tuning parameters (called gains) ensures that ODrive can control your motors in the most effective way possible. The three values are: -* `.controller.config.pos_gain = 20.0` [(counts/s) / counts] -* `.controller.config.vel_gain = 5.0 / 10000.0` [A/(counts/s)] -* `.controller.config.vel_integrator_gain = 10.0 / 10000.0` [A/((counts/s) * s)] +* `.controller.config.pos_gain = 20.0` [(turn/s) / turn] +* `.controller.config.vel_gain = 0.16 ` [Nm/(turn/s)] +* `.controller.config.vel_integrator_gain = 0.32` [Nm/((turn/s) * s)] An upcoming feature will enable automatic tuning. Until then, here is a rough tuning procedure: * Set vel_integrator_gain gain to 0 diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 6f351070..f9fa7159 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -35,7 +35,7 @@ The recommended tools for ODrive development are: * **ARM GNU Compiler**: For cross-compiling code * **ARM GDB**: For debugging the code and stepping through on the device * **OpenOCD**: For flashing the ODrive with the STLink/v2 programmer - * **Python**: For running the Python tools + * **Python 3**, along with the packages `PyYAML`, `Jinja2` and `jsonschema`: For running the Python tools (`odrivetool`). Also required for compiling firmware. See below for specific installation instructions for your OS. @@ -50,13 +50,22 @@ $ tup --version # should be 0.7.5 or later $ python --version # should be 3.7 or later ``` -#### Linux (Ubuntu) +#### Linux (Ubuntu < 20.04) ```bash sudo add-apt-repository ppa:team-gcc-arm-embedded/ppa sudo apt-get update sudo apt-get install gcc-arm-embedded sudo apt-get install openocd sudo add-apt-repository ppa:jonathonf/tup && sudo apt-get update && sudo apt-get install tup +sudo apt-get install python3 python3-yaml python3-jinja2 python3-jsonschema +``` + +#### Linux (Ubuntu >= 20.04) +```bash +sudo apt install gcc-arm-embedded +sudo apt install openocd +sudo apt install tup +sudo apt install python3 python3-yaml python3-jinja2 python3-jsonschema ``` #### Arch Linux @@ -64,6 +73,7 @@ sudo add-apt-repository ppa:jonathonf/tup && sudo apt-get update && sudo apt-get sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils sudo pacman -S arm-none-eabi-gdb sudo pacman -S tup +sudo pacman -S python python-yaml python-jinja python-jsonschema ``` * [OpenOCD AUR package](https://aur.archlinux.org/packages/openocd/) @@ -73,6 +83,7 @@ First install [Homebrew](https://brew.sh/). Then you can run these commands in T brew install armmbed/formulae/arm-none-eabi-gcc brew cask install osxfuse && brew install tup brew install openocd +pip3 install PyYAML Jinja2 jsonschema ``` #### Windows @@ -85,6 +96,8 @@ Some instructions in this document may assume that you're using a bash command p * __Note 2__: 8-2018-q4-major seems to have a bug on Windows. Please use 7-2018-q2-update. * [Tup](http://gittup.org/tup/index.html) * [GNU MCU Eclipse's Windows Build Tools](https://github.com/gnu-mcu-eclipse/windows-build-tools/releases) +* [Python 3](https://www.python.org/downloads/) + * Install Python packages: `pip install PyYAML Jinja2 jsonschema` * [OpenOCD](https://github.com/xpack-dev-tools/openocd-xpack/releases/). * [ST-Link/V2 Drivers](http://www.st.com/web/en/catalog/tools/FM147/SC1887/PF260219) @@ -98,7 +111,7 @@ __CONFIG_BOARD_VERSION__: The board version you're using. Can be `v3.1`, `v3.2`, __CONFIG_USB_PROTOCOL__: Defines which protocol the ODrive should use on the USB interface. * `native`: The native ODrive protocol. Use this if you want to use the python tools in this repo. Can maybe work with macOS. - * `native-stream`: Like the native ODrive protocol, but the ODrive will treat the USB connection exactly as if it was a UART connection. __ Maybe need to use this if you're on macOS__. This is necessary because macOS doesn't grant our python tools sufficient low-level access to treat the device as the USB device that it is. + * `native-stream`: Like the native ODrive protocol, but the ODrive will treat the USB connection exactly as if it was a UART connection. __You may need to use this if you're on macOS__. This is necessary because macOS doesn't grant our python tools sufficient low-level access to treat the device as the USB device that it is. * `none`: Disable USB. The device will still show up when plugged in but it will ignore any commands. **Note**: There is a second USB interface that is always a serial port. @@ -108,6 +121,8 @@ __CONFIG_UART_PROTOCOL__: Defines which protocol the ODrive should use on the UA * `ascii`: The ASCII protocol. Use this option if you control the ODrive with an Arduino. The ODrive Arduino library is not yet updated to the native protocol. * `none`: Disable UART. +__CONFIG_DEBUG__: Defines wether debugging will be enabled when compiling the firmware; specifically the `-g -gdwarf-2` flags. Note that printf debugging will only function if your tup.config specifies the `USB_PROTOCOL` or `UART_PROTOCOL` as stdout and `DEBUG_PRINT` is defined. See the IDE specific documentation for more information. + You can also modify the compile-time defaults for all `.config` parameters. You will find them if you search for `AxisConfig`, `MotorConfig`, etc.

    @@ -128,7 +143,7 @@ If the flashing worked, you can connect to the board using the [odrivetool](gett

    ## Testing -The script `tools/run_tests.py` runs a sequence of automated tests for several firmware features as well as high power burn-in tests. Some tests only need one ODrive and one motor/encoder pair while other tests need a back-to-back test rig such as [this one](https://cad.onshape.com/documents/026bda35ad5dff4d73c1d37f/w/ae302174f402737e1fdb3783/e/5ca143a6e5e24daf1fe8e434). In any case, to run the tests you need to provide a YAML file that lists the parameters of your test setup. An example can be found at [`tools/test-rig-parallel.yaml`](tools/test-rig-parallel.yaml`). The programmer serial number can be found by running `Firmware/find_programmer.sh` (make sure it has the latest formware from STM). +The script `tools/run_tests.py` runs a sequence of automated tests for several firmware features as well as high power burn-in tests. Some tests only need one ODrive and one motor/encoder pair while other tests need a back-to-back test rig such as [this one](https://cad.onshape.com/documents/026bda35ad5dff4d73c1d37f/w/ae302174f402737e1fdb3783/e/5ca143a6e5e24daf1fe8e434). In any case, to run the tests you need to provide a YAML file that lists the parameters of your test setup. An example can be found at [`tools/test-rig-parallel.yaml`](tools/test-rig-parallel.yaml`). The programmer serial number can be found by running `Firmware/find_programmer.sh` (make sure it has the latest firmware from STM).
    The test script commands the ODrive to high currents and high motor speeds so if your ODrive is connected to anything other than a stirdy test-rig (or free spinning motors), it will probably break your machine.
    @@ -243,9 +258,14 @@ To run the docs server locally: ```bash cd docs -gem install bundler -bundle install --path ruby-bundle -bundle exec jekyll serve --host=0.0.0.0 +gem install bundler # The gem command typically comes with a Ruby installation +#export PATH="$PATH:~/.gem/ruby/2.7.0/bin" # or similar (depends on OS) +rm Gemfile.lock # only if below commands cause trouble +bundle config path ruby-bundle +bundle install +mkdir -p _api _includes +python ../Firmware/interface_generator_stub.py --definitions ../Firmware/odrive-interface.yaml --template _layouts/api_documentation_template.j2 --outputs _api/'#'.md && python ../Firmware/interface_generator_stub.py --definitions ../Firmware/odrive-interface.yaml --template _layouts/api_index_template.j2 --output _includes/apiindex.html +bundle exec jekyll serve --incremental --host=0.0.0.0 ``` ## Releases diff --git a/docs/encoders.md b/docs/encoders.md index cb038850..dfe27fd8 100644 --- a/docs/encoders.md +++ b/docs/encoders.md @@ -62,15 +62,15 @@ The following are examples of values that MAY impact the success of calibration. * `.encoder.config.calib_range = 0.05` helps to relax the accuracy of encoder counts during calibration * `.motor.config.calibration_current = 10.0` _sometimes_ needed if this is a large motor * `.motor.config.resistance_calib_max_voltage = 12.0` _sometimes_ needed depending on motor -* `.controller.config.vel_limit = 50000` low values result in the spinning motor stopping abruptly during calibration +* `.controller.config.vel_limit = 5` [turn/s] low values result in the spinning motor stopping abruptly during calibration -Lots of other values can get you. It's a process. Thankfully there is a lot of good people that will help you debug calibration problems. +Lots of other values can get you. It's a process. Thankfully there are a lot of good people that will help you debug calibration problems. If calibration works, congratulations. Now try: * `.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL` -* `.controller.set_vel_setpoint(3000,0) ` +* `.controller.input_vel = 1.5` let it loop a few times and then set: * `.requested_state = AXIS_STATE_IDLE` @@ -118,10 +118,10 @@ must reflect the number of counts odrive receives after one complete turn of the You will probably never be able to properly debug if you have problems unless you use an oscilloscope. If you have one, try the following: Connect to the AB pins, see if you get square waves as you turn the motor. Connect to the I pin, see if you get a pulse on a complete rotation. Sometimes this is hard to see. -If you are using SPI, have a lot at the signal on the CLK, and CS pins. There are many examples on the net for how these should behave. +If you are using SPI, use a logic analyzer and connect a wire to the CLK, MISO, and CS pins. Set a trigger for the CS pin and insure that the encoder position is being sent and is increasing/decreasing as you spin the motor. There is extremely cheap [Sigrok](https://sigrok.org/) supported hardware available for protocol analysis. ## Encoder Noise -Noise is found in all circuits, life is just about figuring out if it is preventing your system from working. Lots of users have no problems with noise interfering with their odrive operation, others will tell you "_I've been using the same encoder as you with no problems_". Power to 'em, that may be true, but it doesn't mean it will work for you. If you are concerned about noise, there are several possible sources: +Noise is found in all circuits, life is just about figuring out if it is preventing your system from working. Lots of users have no problems with noise interfering with their ODrive operation, others will tell you "_I've been using the same encoder as you with no problems_". Power to 'em, that may be true, but it doesn't mean it will work for you. If you are concerned about noise, there are several possible sources: * Importantly, encoder wires may be too close to motor wires, avoid overlap as much as possible * Long wires between encoder and ODrive @@ -135,26 +135,33 @@ If you are using an encoder with an index signal, another problem that has been * when performing an index_search, the motor does not return to the same position each time. One easy step that _might_ fix the noise on the Z input has been to solder a 22nF-47nF capacitor to the Z pin and the GND pin on the underside of the ODrive board. -## AS5047/AS5048 Encoders -The AS5047/AS5048 encoders are Hall Effect/Magnetic sensors that can serve as rotary encoders for the ODrive. -The AS5047 has 3 independent output interfaces: SPI, ABI, and PWM. -The AS5048 has 4 independent output interfaces: SPI, ABI, I2C, and PWM. +## SPI Encoders -Both chips come with evaluation boards that can simplify mounted the chips to your motor. For our purposes if you are using an evaluation board you should select the settings for 3.3v, and tie MOSI high to 3.3v. +Apart from (incremental) quadrature encoders, ODrive also supports absolute SPI encoders (since firmware v0.5). These are usually based on are Hall Effect/Magnetic sensors and measure an absolute angle. This means you don't need to repeat the encoder calibration after every ODrive reboot. Currently, the following modes are supported: -If you are having calibration problems - make sure your magnet is centered on the axis of rotation on the motor, some users report this has a significant impact on calibration. Also make sure your magnet height is within range of the spec sheet. + * **CUI protocol**: Compatible with the AMT23xx family (AMT232A, AMT232B, AMT233A, AMT233B). + * **AMS protocol**: Compatible with AS5047P and AS5048A/AS5048B. -#### Using ABI. -You can use ABI with the AS5047/AS5048 with the default ODrive firmware. For your wiring, connect A, B, 3.3v, GND to the labeled pins on the odrive -The acronym I and Z mean the same thing, connect those as well if you are using an index signal. +Some of these chips come with evaluation boards that can simplify mounting the chips to your motor. For our purposes if you are using an evaluation board you should select the settings for 3.3v. -#### Using SPI. -TobinHall has written a [branch](https://github.com/TobinHall/ODrive/tree/Non-Blocking_Absolute_SPI) that supports the SPI option on the AS5047/AS5048. Use his build to flash firmware on your ODrive and connect MISO, SCK, and CS to the labeled pins on the odrive +1. Connect the encoder to the ODrive's SPI interface: + + - The encoder's SCK, MISO (aka "DATA" on CUI encoders), GND and 3.3V should connect to the ODrive pins with the same label. + - The encoder's MOSI should be tied to 3.3V (AMS encoders only. CUI encoders don't have this pin.) + - The encoder's Chip Select (aka nCS/CSn) can be connected to any of the ODrive's GPIOs (caution: GPIOs 1 and 2 are usually used by UART). -Tie MOSI to 3.3v, connect to the SCK, CLK, MISO, GND and 3.2v pins on the ODrive. (note for SPI users, the acronym SCK and CLK mean the same thing, the acronym CSn and CS mean the same thing.) +2. In `odrivetool`, run: -Add these commands to your calibration / startup script: -* `.encoder.config.abs_spi_cs_gpio_pin = 4` or which ever GPIO pin you choose -* `.encoder.config.mode = 257` -* `.axis0.encoder.config.cpr = 2**14` + .encoder.config.abs_spi_cs_gpio_pin = 4 # or which ever GPIO pin you choose + .encoder.config.mode = ENCODER_MODE_SPI_ABS_CUI # or ENCODER_MODE_SPI_ABS_AMS + .encoder.config.cpr = 2**14 # or 2**12 for AMT232A and AMT233A + .save_configuration() + .reboot() + +3. Run the [offset calibration](#encoder-without-index-signal) and then save the calibration with `.save_configuration()`. + The next time you reboot, the encoder should be immediately ready. + +Sometimes the encoder takes longer than the ODrive to start, in which case you need to clear the errors after every restart. + +If you are having calibration problems - make sure your magnet is centered on the axis of rotation on the motor, some users report this has a significant impact on calibration. Also make sure your magnet height is within range of the spec sheet. diff --git a/docs/endstop_figure.png b/docs/endstop_figure.png new file mode 100644 index 00000000..aaec20dd Binary files /dev/null and b/docs/endstop_figure.png differ diff --git a/docs/endstops.md b/docs/endstops.md new file mode 100644 index 00000000..9dc84e6c --- /dev/null +++ b/docs/endstops.md @@ -0,0 +1,133 @@ +# Endstops and Homing + +By default, the ODrive assumes that your motor encoder's zero position is the same as your machine's zero position, but in real life this is rarely the case. In these systems it is useful to allow your motor to move until a physical or electronic device orders the system to stop. That `endstop` can be used as a known reference point. Once the ODrive has hit that position it may then want to move to a final zero, or `home`, position. The process of finding your machine's zero position is known as `homing`. + +ODrive supports the use of its GPIO pins to connect to phyiscal limit switches or other sensors that can serve as endstops. Before you can home your machine, you must be able to adequately control your motor in `AXIS_STATE_CLOSED_LOOP_CONTROL`. + +--- + +## Endstop Configuration +Each axis supports two endstops: `min_endstop` and `max_endstop`. For each endstop, the following properties are accessible through `odrivetool`: + +Name | Type | Default +--- | -- | -- +gpio_num | int | 0 +offset | float | 0.0 +debounce_ms | float | 50.0 +enabled | boolean | false +is_active_high | boolean | false +pullup | boolean | true + +### gpio_num +The GPIO pin number, according to the silkscreen labels on ODrive. Set with these commands: +``` +..max_endstop.config.gpio_num = <1, 2, 3, 4, 5, 6, 7, 8> +..min_endstop.config.gpio_num = <1, 2, 3, 4, 5, 6, 7, 8> +``` + +### enabled +Enables/disables detection of the endstop. If disabled, homing and e-stop cannot take place. Set with: +``` +..max_endstop.config.enabled = +..min_endstop.config.enabled = +``` + +### offset +This is the position of the endstops on the relevant axis, in counts. For example, if you want a position command of `0` to represent a position 100 counts away from the endstop, the offset would be `-100.0` (because the endstop is located at axis position `-100.0`). + +``` +..min_endstop.config.offset = +``` + +This setting is only used for homing. Only the offset of the `min_endstop` is used. + +### debounce_ms +The debouncing time for this endstop. Most switches exhibit some sort of bounce, and this setting will help prevent the switch from triggering repeatedly. It works for both HIGH and LOW transitions, regardless of the setting of `is_active_high`. Debouncing is a good practice for digital inputs, read up on it [here](https://en.wikipedia.org/wiki/Switch). `debounce_ms` has units of miliseconds. + +``` +..max_endstop.config.debounce_ms = +..min_endstop.config.debounce_ms = +``` + +### is_active_high +This is how you configure the endstop to be either "NPN" or "PNP". An "NPN" configuration would be `is_active_high = False` whereas a PNP configuration is `is_active_high = True`. Refer to the following table for more information: + +Typically configuration **1** or **3** is preferred when using mechanical switches as the most common failure mode leaves the switch open. + +### pullup +Match the pullup value to the configuration. If `true`, it enables the GPIO pullup resistor. If `false`, it enables the GPIO pull*down* resistor. + +![Endstop configuration](Endstop_configuration.png) + + +### Example + +If we want to configure a 3D printer-style (configuration 4) minimum endstop for homing on GPIO 5 and we want our motor to move away from the endstop about a quarter turn with a 8192 cpr encoder, we would set: + +``` +..min_endstop.config.gpio_num = 5 +..min_endstop.config.is_active_high = False +..min_endstop.config.offset = -1.0*(8912/4) +..min_endstop.config.enabled = True +``` + +### Testing The Endstops +Once the endstops are configured you can test your endstops for correct functionality. Try activating your endstops and check the states of these variables through odrivetool: + +``` +..max_endstop.endstop_state +..min_endstop.endstop_state +``` + +A state of `True` means the switch is pressed. A state of `False` means the switch is NOT pressed. As simple as that. Give it a try. Click your switches, or put a magnet on your hall switch and see if the states change. + +After testing, don't forget to save and reboot: +``` +.save_configuration() +.reboot() +``` + +--- + +## Homing Configuration +There is one additional configuration parameter specifically for the homing process: + +Name | Type | Default +--- | -- | -- +homing_speed | float | 0.25f + +`homing_speed` is the axis travel speed during homing, in [turns/second]. If you are using SPI based encoders and the axis is homing in the wrong direction, you can enter a negative value for the homing speed and a negative value for the minimum endstop offset. + + +### Performing the Homing Sequence +Homing is possible once the ODrive has closed-loop control over the axis. To trigger homing, we must enter `AXIS_STATE_HOMING`. This starts the homing sequence, which works as follows: + +1. The axis switches to `INPUT_MODE_VEL_RAMP` +2. The axis ramps up to `homing_speed` in the direction of `min_endstop` +3. The axis presses the `min_endstop` +4. The axis switches to `INPUT_MODE_TRAP_TRAJ` +5. The axis moves to the home position in a controlled manner + +It requires quite a few settings in addition to the endstop settings: + +``` +..controller.config.vel_ramp_rate +..trap_traj.config.vel_limit +..trap_traj.config.accel_limit +..trap_traj.config.decel_limit +``` + +We realize this is a little excessive and we will work towards minimizing the setup, but this works well for smooth and reliable behaviour for now. + +### Homing at Startup +It is possible to configure the odrive to enter homing immediately after startup. To enable homing at startup, the following must be configured: + +``` +..config.startup_homing = True +``` + +## Additional endstop devices + +In addition to phyiscal switches there are other options for wiring up your endstops - you will have to work out the details of connecting your device but here are some suggested approaches: + +![endstop figure](endstop_figure.png) diff --git a/docs/getting-started.md b/docs/getting-started.md index 9cd6ecf2..94bcf15e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -17,6 +17,7 @@ permalink: / - [Configure M0](#configure-m0) - [Position control of M0](#position-control-of-m0) - [Other control modes](#other-control-modes) +- [Watchdog Timer](#watchdog-timer) - [What's next?](#whats-next) @@ -126,7 +127,13 @@ Try step 5 again ### Linux 1. [Install Python 3](https://www.python.org/downloads/). (for example, on Ubuntu, `sudo apt install python3 python3-pip`) 2. Install the ODrive tools by opening a terminal and typing `sudo pip3 install odrive` Enter -3. (needed on Ubuntu, maybe other distros too) Add odrivetool into the path, by adding `~/.local/bin/` into `~/.bash_profile`, for example by running `nano ~/.bashrc`, scrolling to the bottom, pasting `PATH=$PATH:~/.local/bin/`, and then saving and closing, and close and reopen the terminal window. + * This should automatically add the udev rules. If this fails for some reason you can add them manually: + ```bash + echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d[0-9][0-9]", MODE="0666"' | sudo tee /etc/udev/rules.d/91-odrive.rules + sudo udevadm control --reload-rules + sudo udevadm trigger + ``` +3. **Ubuntu**, **Raspbian**: If you can't invoke `odrivetool` at this point, try adding `~/.local/bin` to your `$PATH` ([see related bug](https://unix.stackexchange.com/a/392710/176715)). This is done for example by running `nano ~/.bashrc`, scrolling to the bottom, pasting `export PATH=$PATH:~/.local/bin`, and then saving and closing, and close and reopen the terminal window. ## Firmware **ODrive v3.5 and later**
    @@ -176,7 +183,7 @@ The largest effect on modulation magnitude is speed. There are other smaller fac
    **Velocity limit**
    -`odrv0.axis0.controller.config.vel_limit` [counts/s]. +`odrv0.axis0.controller.config.vel_limit` [turn/s]. The motor will be limited to this speed. Again the default value is quite slow. **Calibration current**
    @@ -187,9 +194,10 @@ You can change `odrv0.axis0.motor.config.calibration_current` [A] to the largest This is the resistance of the brake resistor. If you are not using it, you may set it to `0`. Note that there may be some extra resistance in your wiring and in the screw terminals, so if you are getting issues while braking you may want to increase this parameter by around 0.05 ohm. `odrv0.axis0.motor.config.pole_pairs` -This is the number of **magnet poles** in the rotor, **divided by two**. To find this, you can simply count the number of permanent magnets in the rotor, if you can see them. -**Note**: This is **not** the same as the number of coils in the stator. -If you can't see them, try sliding a loose magnet in your hand around the rotor, and counting how many times it stops. This will be the number of _pole pairs_. If you use a magnetic piece of metal instead of a magnet, you will get the number of _magnet poles_. +This is the number of **magnet poles** in the rotor, **divided by two**. To find this, you can simply count the number of permanent magnets in the rotor, if you can see them. +**Note**: This is **not** the same as the number of coils in the stator. +A good way to find the number of pole pairs is with a current limited power supply. Connect any two of the three phases to a power supply outputting around 2A, spin the motor by hand, and count the number of detents. This will be the number of pole pairs. If you can't distinguish the detents from the normal cogging present when the motor is disconnected, increase the current. +Another way is sliding a loose magnet in your hand around the rotor, and counting how many times it stops. This will be the number of _pole pairs_. If you use a ferrous piece of metal instead of a magnet, you will get the number of _magnet poles_. `odrv0.axis0.motor.config.motor_type` This is the type of motor being used. Currently two types of motors are supported: High-current motors (`MOTOR_TYPE_HIGH_CURRENT`) and gimbal motors (`MOTOR_TYPE_GIMBAL`). @@ -213,13 +221,13 @@ This is 4x the Pulse Per Revolution (PPR) value. Usually this is indicated in th * If you wish to run in sensorless mode, please see [Setting up sensorless](commands.md#setting-up-sensorless). * If you are using hall sensor feedback, please see the [hoverboard motor example](hoverboard.md). +**If using motor thermistor**
    +Please see the [Thermistors](thermistors.md) page for setup. ### 3. Save configuration You can save all `.config` parameters to persistent memory so the ODrive remembers them between power cycles. * `odrv0.save_configuration()` Enter. -Due to a [known issue](https://github.com/madcowswe/ODrive/issues/183) it is strongly recommended that you reboot following every save of your configuration using `odrv0.reboot()`. - ## Position control of M0 Let's get motor 0 up and running. The procedure for motor 1 is exactly the same, so feel free to substitute `axis0` wherever it says `axis0`. @@ -242,7 +250,7 @@ Let's get motor 0 up and running. The procedure for motor 1 is exactly the same, 2. Type `odrv0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL` Enter. From now on the ODrive will try to hold the motor's position. If you try to turn it by hand, it will fight you gently. That is unless you bump up `odrv0.axis0.motor.config.current_lim`, in which case it will fight you more fiercely. If the motor begins to vibrate either immediately or after being disturbed you will need to [lower the controller gains](control.md). -3. Send the motor a new position setpoint. `odrv0.axis0.controller.pos_setpoint = 10000` Enter. The units are in encoder counts. +3. Send the motor a new position setpoint. `odrv0.axis0.controller.input_pos = 1` Enter. The units are in turns. 4. At this point you will probably want to [Properly tune](control.md) the motor controller in order to maximize system performance. ## Other control modes @@ -251,6 +259,7 @@ The default control mode is unfiltered position control in the absolute encoder You may also wish to control velocity (directly or with a ramping filter). You can also directly control the current of the motor, which is proportional to torque. +- [Filtered position control](#filtered-position-control) - [Trajectory control](#trajectory-control) - [Circular position control](#circular-position-control) - [Velocity control](#velocity-control) @@ -258,8 +267,19 @@ You can also directly control the current of the motor, which is proportional to - [Current control](#current-control) +### Filtered position control +Asking the ODrive controller to go as hard as it can to raw setpoints may result in jerky movement. Even if you are using a planned trajectory generated from an external source, if that is sent at a modest frequency, the ODrive may chase each stair in the incoming staircase in a jerky way. In this case, a good starting point for tuning the filter bandwidth is to set it to one half of your setpoint command rate. + +You can use the second order position filter in these cases. +Set the filter bandwidth: `axis.controller.config.input_filter_bandwidth = 2.0` [1/s]
    +Activate the setpoint filter: `axis.controller.config.input_mode = INPUT_MODE_POS_FILTER`.
    +You can now control the velocity with `axis.controller.input_pos = 1` [turns]. + +![secondOrderResponse](secondOrderResponse.PNG)
    +Step response of a 1000 to 0 position input with a filter bandwidth of 1.0 [/sec]. + ### Trajectory control -While in position control mode, use the `move_to_pos` or `move_incremental` functions. See the **Usage** section for details
    +See the **Usage** section for usage details.
    This mode lets you smoothly accelerate, coast, and decelerate the axis from one position to another. With raw position control, the controller simply tries to go to the setpoint as quickly as possible. Using a trajectory lets you tune the feedback gains more aggressively to reject disturbance, while keeping smooth motion. ![Taptraj](TrapTrajPosVel.PNG)
    @@ -270,26 +290,31 @@ In the above image blue is position and orange is velocity. ..trap_traj.config.vel_limit = ..trap_traj.config.accel_limit = ..trap_traj.config.decel_limit = -..trap_traj.config.A_per_css = +..controller.config.inertia = ``` `vel_limit` is the maximum planned trajectory speed. This sets your coasting speed.
    -`accel_limit` is the maximum acceleration in counts / sec^2
    -`decel_limit` is the maximum deceleration in counts / sec^2
    -`A_per_css` is a value which correlates acceleration (in counts / sec^2) and motor current. It is 0 by default. It is optional, but can improve response of your system if correctly tuned. Keep in mind this will need to change with the load / mass of your system. +`accel_limit` is the maximum acceleration in turns / sec^2
    +`decel_limit` is the maximum deceleration in turns / sec^2
    +`controller.config.inertia` is a value which correlates acceleration (in turns / sec^2) and motor torque. It is 0 by default. It is optional, but can improve response of your system if correctly tuned. Keep in mind this will need to change with the load / mass of your system. All values should be strictly positive (>= 0). -Keep in mind that you must still set your safety limits as before. I recommend you set these a little higher ( > 10%) than the planner values, to give the controller enough control authority. +Keep in mind that you must still set your safety limits as before. It is recommended you set these a little higher ( > 10%) than the planner values, to give the controller enough control authority. ``` ..motor.config.current_lim = ..controller.config.vel_limit = ``` #### Usage -Use the `move_to_pos` function to move to an absolute position: +Make sure you are in position control mode. To activate the trajectory module, set the input mode to trajectory: ``` -..controller.move_to_pos(your_absolute_pos) +axis.controller.config.input_mode = INPUT_MODE_TRAP_TRAJ +``` + +Simply send a position command to execute the move: +``` +..controller.input_pos = ``` Use the `move_incremental` function to move to a relative position. @@ -303,32 +328,31 @@ You can also execute a move with the [appropriate ascii command](ascii-protocol. ### Circular position control -To enable Circular position control, set `axis.controller.config.setpoints_in_cpr = True` +To enable Circular position control, set `axis.controller.config.circular_setpoints = True` -This mode is useful for continuos incremental position movement. For example a robot rolling indefinitely, or an extruder motor or conveyor belt moving with controlled increments indefinitely. -In the regular position mode, the `pos_setpoint` would grow to a very large value and would lose precision due to floating point rounding. +This mode is useful for continuous incremental position movement. For example a robot rolling indefinitely, or an extruder motor or conveyor belt moving with controlled increments indefinitely. +In the regular position mode, the `input_pos` would grow to a very large value and would lose precision due to floating point rounding. -In this mode, the controller will try to track the position within only one turn of the motor. Specifically, `pos_setpoint` is expected in the range `[0, cpr-1]`, where `cpr` is the number of encoder counts in one revolution. If the `pos_setpoint` is incremented to outside this range (say via step/dir input), it is automatically wrapped around into the correct value. -Note that in this mode `encoder.pos_cpr` is used for feedback in stead of `encoder.pos_estimate`. +In this mode, the controller will try to track the position within only one turn of the motor. Specifically, `input_pos` is expected in the range `[0, 1)`. If the `input_pos` is incremented to outside this range (say via step/dir input), it is automatically wrapped around into the correct value. +Note that in this mode `encoder.pos_circular` is used for feedback instead of `encoder.pos_estimate`. -If you try to increment the axis with a large step in one go that exceeds `cpr/2` steps, the motor will go to the same angle around the wrong way. This is also the case if there is a large disturbance. If you have an application where you would like to handle larger steps, you can use a virtual CPR that is an integer times larger than your encoder's actual CPR. Set `encoder.config.cpr = N * your_enc_cpr`, where N is some integer. Choose N to give you an appropriate circular space for your application. +If you try to increment the axis with a large step in one go that exceeds `1` turn, the motor will go to the same angle around the wrong way. This is also the case if there is a large disturbance. If you have an application where you would like to handle larger steps, you can use a larger circular range. Set `controller.config.circular_setpoints_range = N`. Choose N to give you an appropriate circular space for your application. ### Velocity control -Set `axis.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL`.
    -You can now control the velocity with `axis.controller.vel_setpoint = 5000` [count/s]. +Set `axis.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL`.
    +You can now control the velocity with `axis.controller.input_vel = 1` [turn/s]. ### Ramped velocity control -Set `axis.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL`.
    -Set the velocity ramp rate (acceleration): `axis.controller.config.vel_ramp_rate = 2000` [counts/s^2]
    -Activate the ramped velocity mode: `axis.controller.vel_ramp_enable = True`.
    -You can now control the velocity with `axis.controller.vel_ramp_target = 5000` [count/s]. +Set `axis.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL`.
    +Set the velocity ramp rate (acceleration): `axis.controller.config.vel_ramp_rate = 0.5` [turn/s^2]
    +Activate the ramped velocity mode: `axis.controller.config.input_mode = INPUT_MODE_VEL_RAMP`.
    +You can now control the velocity with `axis.controller.input_vel = 1` [turn/s]. -### Current control -Set `axis.controller.config.control_mode = CTRL_MODE_CURRENT_CONTROL`.
    -You can now control the current with `axis.controller.current_setpoint = 3` [A]. - -*Note: There is no velocity limiting in current control mode. Make sure that you don't overrev the motor, or exceed the max speed for your encoder.* +### Torque control +Set `axis.controller.config.control_mode = CONTROL_MODE_TORQUE_CONTROL`.
    +You can now control the torque with `axis.controller.input_torque = 0.1` [Nm]. +Note: If you exceed `vel_limit` in current control mode, the current is reduced. To disable this, set `axis.controller.enable_current_mode_vel_limit = False`. ## Watchdog Timer Each axis has a configurable watchdog timer that can stop the motors if the diff --git a/docs/hoverboard.md b/docs/hoverboard.md index 730391fd..93938613 100644 --- a/docs/hoverboard.md +++ b/docs/hoverboard.md @@ -52,7 +52,7 @@ odrv0.axis0.controller.config.pos_gain = 1 odrv0.axis0.controller.config.vel_gain = 0.02 odrv0.axis0.controller.config.vel_integrator_gain = 0.1 odrv0.axis0.controller.config.vel_limit = 1000 -odrv0.axis0.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL +odrv0.axis0.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL ``` In the next step we are going to start powering the motor and so we want to make sure that some of the above settings that require a reboot are applied first. @@ -112,9 +112,9 @@ The ODrive starts in idle (we will look at changing this later) so we can enable odrv0.save_configuration() odrv0.reboot() odrv0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL -odrv0.axis0.controller.vel_setpoint = 120 +odrv0.axis0.controller.input_vel = 120 # Your motor should spin here -odrv0.axis0.controller.vel_setpoint = 0 +odrv0.axis0.controller.input_vel = 0 odrv0.axis0.requested_state = AXIS_STATE_IDLE ``` @@ -128,31 +128,31 @@ We also have to reboot to activate the PWM input. ```txt odrv0.config.gpio3_pwm_mapping.min = -200 odrv0.config.gpio3_pwm_mapping.max = 200 -odrv0.config.gpio3_pwm_mapping.endpoint = odrv0.axis0.controller._remote_attributes['vel_setpoint'] +odrv0.config.gpio3_pwm_mapping.endpoint = odrv0.axis0.controller._remote_attributes['input_vel'] odrv0.config.gpio4_pwm_mapping.min = -200 odrv0.config.gpio4_pwm_mapping.max = 200 -odrv0.config.gpio4_pwm_mapping.endpoint = odrv0.axis1.controller._remote_attributes['vel_setpoint'] +odrv0.config.gpio4_pwm_mapping.endpoint = odrv0.axis1.controller._remote_attributes['input_vel'] odrv0.save_configuration() odrv0.reboot() ``` -Now we can check that the sticks are writing to the velocity setpoint. Move the stick, print `vel_setpoint`, move to a different position, check again. +Now we can check that the sticks are writing to the velocity setpoint. Move the stick, print `input_vel`, move to a different position, check again. ```txt -In [1]: odrv0.axis1.controller.vel_setpoint +In [1]: odrv0.axis1.controller.input_vel Out[1]: 0.1904754638671875 -In [2]: odrv0.axis1.controller.vel_setpoint +In [2]: odrv0.axis1.controller.input_vel Out[2]: 0.1904754638671875 -In [3]: odrv0.axis1.controller.vel_setpoint +In [3]: odrv0.axis1.controller.input_vel Out[3]: 28.152389526367188 -In [4]: odrv0.axis1.controller.vel_setpoint +In [4]: odrv0.axis1.controller.input_vel Out[4]: 61.21905517578125 -In [5]: odrv0.axis1.controller.vel_setpoint +In [5]: odrv0.axis1.controller.input_vel Out[5]: -52.990474700927734 ``` diff --git a/docs/interface-definition-file.md b/docs/interface-definition-file.md new file mode 100644 index 00000000..9e4628e1 --- /dev/null +++ b/docs/interface-definition-file.md @@ -0,0 +1,133 @@ +# Interface Definition File + +This document describes the rules on which the ODrive Interface Definition file is built. It is intended for ODrive contributors who wish to modify it or ODrive users who want to autogenerate their own code from this file to interface with the ODrive. + +## Terms and Concepts + +*Value types* are a way of saying how values of this type are serialized/deserialized to/from raw bytes. +Value types can be: + - one of the well-known types `bool`, `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int32`, `uint32`, `int64`, `uint64`, `float32`, `float64`, `fibre.Ref` + - An enumeration (that is, a mapping between serialized numbers and well known value names) + - A set of flags (in many programming languages this is the same as normal enums) + +An *interface* is a collection of features (attributes and functions) that can be implemented by an object or used by a client as a filter for object discovery. + +A *function* is something that takes zero or more inputs from the client, does something, and then returns zero or more outputs to the client. Since these input and output arguments are transmitted as raw bytes, they each have a value type. + +An *attribute* is a reference to a subobject which again implements some interface. + +Many languages don't make clear distinctions between interfaces and value types so let's be clear on this: attributes _always_ have an interface type and function input/output arguments _always_ have a value type. If you see something that looks like an attribute with a value type (let's say `uint32`), it's actually an attribute with the interface type `fibre.Property`. If you see a function argument that looks like an interface type (let's say `MyIntf`) it's actually of the value type `fibre.Ref`. + + +## File Structure + +The top level contains a dictionary of interfaces and a dictionary of value types. +Interfaces as well as value types can be subordinate to other interfaces. Nested names are specified using dots in between the subnames. + +Example: + +```yaml +interfaces: + MyFirstInterface: ... + MyFirstInterface.SubInterface: ... + +valuetypes: + MyFirstEnum: ... + MyFirstInterface.SubEnum: ... +``` + +## Interfaces + +Interfaces consist of an `attributes` dictionary and a `functions` dictionary. + +**Attributes** have a type which is either given by name as a string or directly in place. +Even though attributes conceptually and internally are always resolved to an interface type, for your convenience you can also give a value type which is then implicitly resolved to `fibre.Property`. + +If the type is given as a string, it is resolved based on the scope in which it occurs. The search precedence is as follows: The innermost scope is searched first for an interface with that name and then for a value type with that name. If both names don't exist, the next outer scope is checked. Note that the order in which types are defined does not matter. The whole file is read before any type resolution occurs. + +**Functions** have an `in` and `out` dictionary specifying one or more argument names with their corresponding value types. Like with attributes, the types can be specified in place or as a name. Type resolution also works the same except that only value types are checked for. + +Example: +```yaml +interfaces: + Car: + attributes: + velocity: float + door_front_left: Door + door_front_right: Door + steering_wheel: + attributes: + angle: float + functions: + turn: {in: {delta_angle: float32}, out: {final_angle: float32}} + Car.Door: + attributes: + is_open: bool + part_of: Car + functions: + open: + close: +``` + +Let's see how the type resolution of the attibute `Car.Door.part_of: Car` would work here: + + 1. Interface `Car.Door.Car` => not found, proceed + 2. Value type `Car.Door.Car` => not found, proceed + 3. Interface `Car.Car` => not found, proceed + 4. Value type `Car.Car` => not found, proceed + 5. Interface `Car` => found. Link to this interface type. + + +## Enums + +Enums are values which are associated with a name. They are serialized as 32-bit numbers. + +Enumerators without an explicitly stated numerical value are guaranteed to have an underlying value one larger than that of the preceding enumerator. + +Each enumerator must have a unique value. + +Example: + +```yaml +valuetypes: + ModeOfTransport: + values: + Walking: + Bicycle: + Car: {value: 5} + Train: +``` + +This would be serialized as: + - Walking <=> `0x00000000` <=> `0x00 0x00 0x00 0x00` + - Bicycle <=> `0x00000001` <=> `0x01 0x00 0x00 0x00` + - Car <=> `0x00000005` <=> `0x05 0x00 0x00 0x00` + - Train <=> `0x00000006` <=> `0x06 0x00 0x00 0x00` + +## Flagfields + +Flagfields are serialized as 32-bit low endian values where each bit has a named meaning. + +A flag without an explicit bit number is guaranteed to have the bit number of the preceding flag plus one or bit 0 it it's the first in the list. + +Each flag must have a unique bit number. + +Example: + +```yaml +valuetypes: + Anchor: + nullflag: Nowhere + flags: + Top: + Left: + Bottom: {bit: 8} + Right: +``` + +This would be serialized as: + - Nowhere <=> `0x00000000` <=> `0x00 0x00 0x00 0x00` + - Top <=> `0x00000001` <=> `0x01 0x00 0x00 0x00` + - Top and Left <=> `0x00000003` <=> `0x03 0x00 0x00 0x00` + - Bottom <=> `0x00000100` <=> `0x00 0x01 0x00 0x00` + - Top and Bottom and Right <=> `0x00000301` <=> `0x01 0x03 0x00 0x00` diff --git a/docs/interfaces.md b/docs/interfaces.md index e9a8781d..ebe279e9 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -102,7 +102,7 @@ Pinout: To enable step/dir mode for the GPIO, set `.config.enable_step_dir` to true for each axis that you wish to use this on. Axis 0 step/dir pins conflicts with UART, and the UART takes priority. So to be able to use step/dir on Axis 0, you must also set `odrv0.config.enable_uart = False`. See the [pin function priorities](#pin-function-priorities) for more detail. Don't forget to save configuration and reboot. -There is also a config variable called `.config.counts_per_step`, which specifies how many encoder counts a "step" corresponds to. It can be any floating point value. +There is also a config variable called `.config.turns_per_step`, which specifies how many turns a "step" corresponds to. The default value is 1.0f/1024.0f. It can be any floating point value. The maximum step rate is pending tests, but it should handle at least 50kHz. If you want to test it, please be aware that the failure mode on too high step rates is expected to be that the motors shuts down and coasts. Please be aware that there is no enable line right now, and the step/direction interface is enabled by default, and remains active as long as the ODrive is in position control mode. To get the ODrive to go into position control mode at bootup, see how to configure the [startup procedure](commands.md#startup-procedure). @@ -113,15 +113,15 @@ You can control the ODrive directly from an hobby RC receiver. Some GPIO pins can be used for PWM input, if they are not allocated to other functions. For example, you must disable the UART to use GPIO 1,2. See the [pin function priorities](#pin-function-priorities) for more detail. Any of the numerical parameters that are writable from the ODrive Tool can be hooked up to a PWM input. -As an example, we'll configure GPIO4 to control the angle of axis 0. We want the axis to move within a range of -1500 to 1500 encoder counts. +As an example, we'll configure GPIO4 to control the angle of axis 0. We want the axis to move within a range of -2 to 2 turns. -1. Make sure you're able control the axis 0 angle by writing to `odrv0.axis0.controller.pos_setpoint`. If you need help with this follow the [getting started guide](getting-started.md). +1. Make sure you're able control the axis 0 angle by writing to `odrv0.axis0.controller.input_pos`. If you need help with this follow the [getting started guide](getting-started.md). 2. If you want to control your ODrive with the PWM input without using anything else to activate the ODrive, you can configure the ODrive such that axis 0 automatically goes operational at startup. See [here](commands.md#startup-procedure) for more information. 3. In ODrive Tool, configure the PWM input mapping ``` - In [1]: odrv0.config.gpio4_pwm_mapping.min = -1500 - In [2]: odrv0.config.gpio4_pwm_mapping.max = 1500 - In [3]: odrv0.config.gpio4_pwm_mapping.endpoint = odrv0.axis0.controller._remote_attributes['pos_setpoint'] + In [1]: odrv0.config.gpio4_pwm_mapping.min = -2 + In [2]: odrv0.config.gpio4_pwm_mapping.max = 2 + In [3]: odrv0.config.gpio4_pwm_mapping.endpoint = odrv0.axis0.controller._remote_attributes['input_pos'] ``` Note: you can disable the input by setting `odrv0.config.gpio4_pwm_mapping.endpoint = None` 4. Save the configuration and reboot @@ -164,8 +164,12 @@ The endpoint pairs `0x01, 0x81` and `0x03, 0x83` behave exactly identical, only If you plan to access the USB endpoints directly it is recommended that you use interface 2. The other interfaces (the ones associated with the CDC device) are usually claimed by the CDC driver of the host OS, so their endpoints cannot be used without first detaching the CDC driver. ### UART -Baud rate: 115200 +Baud rate: 115200 by default. See `odrv0.config.uart_baudrate` to change value. Requires a restart. Pinout: * GPIO 1: Tx (connect to Rx of other device) * GPIO 2: Rx (connect to Tx of other device) * GND: you must connect the grounds of the devices together. Use any GND pin on J3 of the ODrive. + +## CAN Simple Protocol + +See [CAN Protocol](can-protocol). diff --git a/docs/odrivetool.md b/docs/odrivetool.md index cb8059c3..3cd01972 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -88,7 +88,7 @@ To compile firmware from source, refer to the [developer guide](developer-guide) * On some machines you will need to unplug and plug back in the USB cable to make the PC understand that we switched from regular mode to bootloader mode. * If the DFU script can't find the device, try forcing it into DFU mode. -
    How to force DFU mode (ODrive v3.5)
    +
    How to force DFU mode (ODrive v3.5 and newer)
    Flick the DIP switch that "DFU, RUN" to "DFU" and power cycle the board. After you're done upgrading firmware, don't forget to put the switch back into the "RUN" position and power cycle the board again.
    @@ -211,13 +211,13 @@ To change what parameters are plotted open odrivetool (located in Anaconda3\Scri my_odrive.axis1.encoder.pos_estimate, ]) ``` -For example, to plot the approximate motor torque [N.cm] and the velocity [RPM] of axis1 with a 150KV motor and an 8192 count per rotation econder you would modify the function to read: +For example, to plot the approximate motor torque [Nm] and the velocity [RPM] of axis0, you would modify the function to read: ``` # If you want to plot different values, change them here. # You can plot any number of values concurrently. cancellation_token = start_liveplotter(lambda: [ - (((my_odrive.axis0.encoder.pll_vel)/8192)*60), # 8192 CPR encoder - ((8.27*my_odrive.axis0.motor.current_control.Iq_setpoint/150) * 100), # Torque [N.cm] = (8.27 * Current [A] / KV) * 100 + ((my_odrive.axis0.encoder.vel_estimate*60), # turns/s to rpm + ((my_odrive.axis0.motor.current_control.Iq_setpoint * my_odrive.axis0.motor.config.torque_constant), # Torque [Nm] ]) ``` In the example below the motor is forced off axis by hand and held there. In response the motor controller increases the torque (orange line) to counteract this disturbance up to a peak of 500 N.cm at which point the motor current limit is reached. When the motor is released it returns back to its commanded position very quickly as can be seen by the spike in the motor velocity (blue line). diff --git a/docs/protocol.md b/docs/protocol.md index 6e8809d6..fd244e65 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -45,7 +45,7 @@ __Request__ - The length of the payload is determined by the total packet size. The format of the payload depends on the endpoint type. The endpoint type can be obtained from the JSON definition. - __Bytes N-2, N-1__ - For endpoint 0: Protocol version (currently 1). A server shall ignore packets with other values. - - For all other endpoints: The CRC16 calculated over the JSON definition. The CRC16 init value is the protocol version (currently 1). A server shall ignore packets that set this field incorrectly. See protocol.hpp for CRC details. + - For all other endpoints: The CRC16 calculated over the JSON definition using the algorithm described below, except that the initial value is set to the protocol version (currently 1). A server shall ignore packets that set this field incorrectly. __Response__ @@ -61,8 +61,26 @@ The stream based format is just a wrapper for the packet format. - __Byte 0__ Sync byte `0xAA` - __Byte 1__ Packet length - Currently both parties shall only emit and accept values of 0 through 127. - - __Byte 2__ CRC8 of bytes 0 and 1 - - See protocol.hpp for CRC details. + - __Byte 2__ CRC8 of bytes 0 and 1 (see below for details) - __Bytes 3 to N-3__ Packet - - __Bytes N-2, N-1__ CRC16 - - See protocol.hpp for CRC details. + - __Bytes N-2, N-1__ CRC16 (see below for details) + +## CRC algorithms ## + +__CRC8__ + - Polynomial: `0x37` + - Initial value: `0x42` + - No input reflection, no result reflection, no final XOR operation + - Examples: + - `0x01, 0x02, 0x03, 0x04` => `0x61` + - `0x05, 0x04, 0x03, 0x02, 0x01` => `0x64` + +__CRC16__ + - Polynomial: `0x3d65` + - Initial value: `0x1337` (or `0x0001` for the JSON CRC) + - No input reflection, no result reflection, no final XOR operation + - Examples: + - `0x01, 0x02, 0x03, 0x04` => `0x672E` + - `0x05, 0x04, 0x03, 0x02, 0x01` => `0xE251` + +You can use the online calculator at http://www.sunshine2k.de/coding/javascript/crc/crc_js.html to verify your implementation. diff --git a/docs/screenshots/thermistor-voltage-divider.png b/docs/screenshots/thermistor-voltage-divider.png new file mode 100644 index 00000000..e01c7cf9 Binary files /dev/null and b/docs/screenshots/thermistor-voltage-divider.png differ diff --git a/docs/secondOrderResponse.PNG b/docs/secondOrderResponse.PNG new file mode 100644 index 00000000..7199b37f Binary files /dev/null and b/docs/secondOrderResponse.PNG differ diff --git a/docs/test.md b/docs/test.md deleted file mode 100644 index 9daeafb9..00000000 --- a/docs/test.md +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/docs/testing.md b/docs/testing.md index 347db2b1..5ba1160b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -3,21 +3,120 @@ This section describes how to use the automated testing facilities. You don't have to do this as an end user. -They test the following aspects: - - System functions (communication interfaces, configuration storage) - - Functionality of the motor controller and state machine - - High speed and high load conditions - The testing facility consists of the following components: - * **Test rig:** In the simplest case this can be a single ODrive with a single motor and encoder pair. Can also be multiple ODrives with multiple axes, some of which may be mechanically coupled. + * **Test rig:** In the simplest case this can be a single ODrive optionally with a single motor and encoder pair. Can also be multiple ODrives with multiple axes, some of which may be mechanically coupled. * **Test host:** The PC on which the test script runs. All ODrives must be connected to the test host via USB. * **test-rig.yaml:** Describes your test rig. Make sure all values are correct. Incorrect values may physically break or fry your test setup. - * **run_tests.py:** This is the main script that runs all the tests. + * **test_runner.py:** This is the main script that runs all the tests. + * **..._test.py** The actual tests -## How to run +## The Tests -Example: + - `analog_input_test.py`: Analog Input + - `calibration_test.py`: Motor calibration, encoder offset calibration, encoder direction find, encoder index search + - `can_test.py`: Partial coverage of the commands described in [CAN Protocol](can-protocol) + - `closed_loop_test.py`: Velocity control, position control (TODO: sensorless control), brake regen current hard limit, current control with velocity limiting + - `encoder_test.py`: Incremental encoder, hall effect encoder, sin/cos encoder, SPI encoders (AMS, CUI) + - `fibre_test.py`: General USB protocol tests + - `nvm_test.py`: Configuration storage + - `pwm_input_test.py`: PWM input + - `step_dir_test.py`: Step/dir input + - `uart_ascii_test.py`: Partial coverage of the commands described in [ASCII Protocol](ascii-protocol) + +All tests in a file can be run with e.g.: + + python3 uart_ascii_test.py --test-rig-yaml ../../test-rig-rpi.yaml + +See the following sections for a more detailed test flow description. + +## Our test rig + +Our test rig essentially consists of the following components: + + - an ODrive as the test subject + - a Teensy 4.0 to emulate external hardware such as encoders + - a Motor + Encoder pair for closed loop control tests + - a Raspberry Pi 4.0 as test host + - a CAN hat for the Raspberry Pi for CAN tests + +This document is therefore centered around this test rig layout. +If your test rig differs, you may be able to run some but not all of the tests. + +## How to set up a Raspberry Pi as testing host + + 1. Install Raspbian Lite on a Raspberry Pi 4.0. I used the NOOBS installer for this. + 2. Prepare the installation: + + sudo systemctl enable ssh + sudo systemctl start ssh + # Transfer your public key for passwordless SSH. All subsequent steps can be done via SSH. + sudo apt-get update + sudo apt-get upgrade + + 3. Add the following lines to `/boot/config.txt`: + - `enable_uart=1` + - `dtparam=spi=on` + - `dtoverlay=spi-bcm2835-overlay` + - `dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25` - Note: These oscillator and interrupt GPIO settings here are for the "RS485 CAN HAT" I have. There appear to be multiple versions, so they may be different from yours. Check the marking on the oscillator and the schematics. + + 4. Remove the following arguments from `/boot/cmdline.txt`: + - `console=serial0,115200` + + 5. Reboot. + + 6. Install the prerequisites: + + sudo apt-get install ipython3 python3-appdirs python3-yaml python3-usb python3-serial python3-can python3-scipy git openocd + # Optionally, to be able to compile the firmware: + sudo apt-get install gcc-arm-none-eabi + + 7. Install Teensyduino and teensy-loader-cli: + + sudo apt-get install libfontconfig libxft2 libusb-dev + + wget https://downloads.arduino.cc/arduino-1.8.12-linuxarm.tar.xz + tar -xf arduino-1.8.12-linuxarm.tar.xz + wget https://www.pjrc.com/teensy/td_151/TeensyduinoInstall.linuxarm + chmod +x TeensyduinoInstall.linuxarm + ./TeensyduinoInstall.linuxarm --dir=arduino-1.8.12 + sudo cp -R arduino-1.8.12 /usr/share/arduino + sudo ln -s /usr/share/arduino/arduino /usr/bin/arduino + + git clone https://github.com/PaulStoffregen/teensy_loader_cli + pushd teensy_loader_cli + sudo cp teensy_loader_cli /usr/bin/ + sudo ln -s /usr/bin/teensy_loader_cli /usr/bin/teensy-loader-cli + popd + + 8. Add the following lines to `/etc/udev/rules.d/49-stlinkv2`: + + SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", MODE:="0666" + SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", MODE:="0666" + + 9. `sudo ../../odrivetool udev-setup` + + 10. `sudo udevadm trigger` + + 11. Run once after every reboot: `sudo ipython3 --pdb test_runner.py -- --setup-host --test-rig-yaml ../../test-rig-rpi.yaml` + +## SSH testing flow + +Here's one possible workflow for developing on the local host and testing on a remote SSH host. + +We assume that the ODrive repo is at `/path/to/ODriveFirmware` and your testing host is configured under the SSH name `odrv`. + +To flash and start remote debugging: + + 1. Start OpenOCD remotely, along with a tunnel to localhost: `ssh -t odrv -L3333:localhost:3333 bash -c "\"openocd '-f' 'interface/stlink-v2.cfg' '-f' 'target/stm32f4x_stlink.cfg'\""` + You can keep this open for multiple debug sessions. Press Ctrl+C to quit. + 2. Compile the firmware + 3. In VSCode, select the run configuration "Debug ODrive via external server" and press Run. In contrast to the other configurations, this will flash the new firmware before dropping you into the debugger. + +To run a test: + + rsync -avh -e ssh /path/to/ODriveFirmware/ odrv:/opt/odrivetest --exclude="Firmware/build" --exclude="Firmware/.tup" --exclude=".git" --delete + + ssh odrv + > cd /opt/odrivetest/tools/odrive/tests/ + > ipython3 --pdb uart_ascii_test.py -- --test-rig-yaml ../../test-rig-rpi.yaml -``` -./run_tests.py --skip-boring-tests --ignore top-odrive.yellow bottom-odrive.yellow -``` diff --git a/docs/thermistors.md b/docs/thermistors.md new file mode 100644 index 00000000..4b2a225d --- /dev/null +++ b/docs/thermistors.md @@ -0,0 +1,34 @@ +# Thermistors + +## Introduction +Thermistors are elements that change their resistance based on the temperature. They can be used to electrically measure temperature. The ODrive itself has thermistors on board near the FETs to ensure that they don't burn themselves out. In addition to this it's possible to connect your own thermistor to measure the temperature of the connected motors. There are two types of thermistors, Negative Temperature Coefficient (NTC) and Positive Temperature Coefficient (PTC). This indicates whether the resistance goes up or down when the temperature goes up or down. The ODrive only supports the NTC type thermistor. + +## FET thermistor +The temperature of the onboard FET thermistors can be read out by using the `odrivetool` under `.fet_thermistor.temp`. The odrive will automatically start current limiting the motor when the `.fet_thermistor.config.temp_limit_lower` threshold is exceeded and once `.fet_thermistor.config.temp_limit_upper` is exceeded the ODrive will stop controlling the motor and set an error. The lower and upper threshold can be changed, but this is not recommended. + +## Connecting motor thermistors + +To use your own thermistors with the ODrive a few things have to be clarified first. The use of your own thermistor requires one analog input pin. Under `.motor_thermistor.config` the configuration of your own thermistor is available with the following fields: + +* `gpio_pin`: The GPIO input in used for this thermistor. +* `poly_coefficient_0` to `poly_coefficient_3`: Coefficient that needs to be set for your specific setup more on that in [Thermistor coefficients](#Thermistor coefficients). +* `temp_limit_lower` and `temp_limit_upper`: Same principle as the FET temperature limits. +* `enabled`: Whether this thermistor is enabled or not. + +## Voltage divider circuit +To measure a temperature with a thermistor a voltage divider circuit is used in addition with an ADC. The screenshot below is taken directly from the ODrive schematic. + +![Launch Configurations](screenshots/thermistor-voltage-divider.png "Thermistor voltage divider") + +The way this works is that the thermistor is connected in series with a known resistance value. By connecting an ADC directly after the thermistor the resistance value can be determined. For further information see [Voltage divider](https://en.wikipedia.org/wiki/Voltage_divider). While not strictly necessary, it is a good idea to add a capacitor as shown as well. This will help reduce the effect of electrical noise. A value between 470nF and 4.7uF is recommended, and any voltage rating 4V or higher. Put the capacitor physically close to the ODrive. + +To use a thermistor with the ODrive a voltage divider circuit has to be made that uses `VCCA` as the power source with `GNDA` as the ground. The voltage divider output can be connected to a GPIO pin that supports analog input. + +## Thermistor coefficients +Every thermistor and voltage divider circuit is different and thus it's necessary to let the ODrive know how to relate a voltage it measures at the GPIO pin to a temperature. The `poly_coefficient_0` to `poly_coefficient_3` under `.motor_thermistor.config` are used for this. The `odrivetool` has a convenience function `set_motor_thermistor_coeffs(axis, Rload, R_25, Beta, Tmin, Tmax)` which can be used to calculate and set these coefficients. + +* `axis`: Which axis do set the motor thermistor coefficients for (`odrv0.axis0` or `odrv0.axis1`). +* `Rload`: The Ohm value of the resistor used in the voltage divider circuit. +* `R_25`: The resistance of the thermistor when the temperature is 25 degrees celsius. Can usually be found in the datasheet of your thermistor. Can also be measured manually with a multimeter. +* `Beta`: A constant specific to your thermistor. Can be found in the datasheet of your thermistor. +* `Tmin` and `Tmax`: The temperature range that is used to create the coefficients. Make sure to set this range to be wider than what is expected during operation. A good example may be -10 to 150. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6ba3752e..a06366eb 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -4,10 +4,6 @@ Table of Contents: - [Error codes](#error-codes) -- [Common Axis Errors](#common-axis-errors) -- [Common Motor Errors](#common-motor-errors) -- [Common Encoder Errors](#common-encoder-errors) -- [Common Controller Errors](#common-controller-errors) - [USB Connectivity Issues](#usb-connectivity-issues) - [Firmware Issues](#firmware-issues) - [Other issues that may not produce an error code](#other-issues-that-may-not-produce-an-error-code) @@ -17,94 +13,12 @@ Table of Contents: ## Error codes If your ODrive is not working as expected, run `odrivetool` and type `dump_errors(odrv0)` Enter. This will dump a list of all the errors that are present. To also clear all the errors, you can run `dump_errors(odrv0, True)`. -The following sections will give some guidance on the most common errors. You may also check the code for the full list of errors: -* Axis error flags defined [here](../Firmware/MotorControl/axis.hpp). -* Motor error flags defined [here](../Firmware/MotorControl/motor.hpp). -* Encoder error flags defined [here](../Firmware/MotorControl/encoder.hpp). -* Controller error flags defined [here](../Firmware/MotorControl/controller.hpp). -* Sensorless estimator error flags defined [here](../Firmware/MotorControl/sensorless_estimator.hpp). - -## Common Axis Errors - -* `ERROR_INVALID_STATE = 0x01` - -You tried to run a state before you are allowed to. Typically you tried to run encoder calibration or closed loop control before the motor was calibrated, or you tried to run closed loop control before the encoder was calibrated. - -* `ERROR_DC_BUS_UNDER_VOLTAGE = 0x02` - -Confirm that your power leads are connected securely. For initial testing a 12V PSU which can supply a couple of amps should be sufficient while the use of low current 'wall wart' plug packs may lead to inconsistent behaviour and is not recommended. - -You can monitor your PSU voltage using liveplotter in odrive tool by entering `start_liveplotter(lambda: [odrv0.vbus_voltage])`. If you see your votlage drop below ~ 8V then you will trip this error. Even a relatively small motor can draw multiple kW momentary and so unless you have a very large PSU or are running of a battery you may encounter this error when executing high speed movements with a high current limit. To limit your PSU power draw you can limit your motor current and/or velocity limit `odrv0.axis0.controller.config.vel_limit` and `odrv0.axis0.motor.config.current_lim`. - -* `ERROR_DC_BUS_OVER_VOLTAGE = 0x04` - -Confirm that you have a brake resistor of the correct value connected securly and that `odrv0.config.brake_resistance` is set to the value of your brake resistor. - -You can monitor your PSU voltage using liveplotter in odrive tool by entering `start_liveplotter(lambda: [odrv0.vbus_voltage])`. If during a move you see the voltage rise above your PSU's nominal set voltage then you have your brake resistance set too low. This may happen if you are using long wires or small gauge wires to connect your brake resistor to your odrive which will added extra resistance. This extra resistance needs to be accounted for to prevent this voltage spike. If you have checked all your connections you can also try increasing your brake resistance by ~ 0.01 Ohm at a time to a maximum of 0.05 greater than your brake resistor value. - -## Common Motor Errors - -* `ERROR_PHASE_RESISTANCE_OUT_OF_RANGE = 0x0001` and `ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE = 0x0002` - -During calibration the motor resistance and [inductance](https://en.wikipedia.org/wiki/Inductance) is measured. If the measured motor resistance or inductance falls outside a set range this error will be returned. Check that all motor leads are connected securely. - -The measured values can be viewed using odrivetool as is shown below: -``` -In [2]: odrv0.axis0.motor.config.phase_inductance -Out[2]: 1.408751450071577e-05 - -In [3]: odrv0.axis0.motor.config.phase_resistance -Out[3]: 0.029788672924041748 -``` -Some motors will have a considerably different phase resistance and inductance than this. For example, gimbal motors, some small motors (e.g. < 10A peak current). If you think this applies to you try increasing `odrv0.axis0.motor.config.resistance_calib_max_voltage` from its default value of 1 using odrivetool and repeat the motor calibration process. If your motor has a small peak current draw (e.g. < 20A) you can also try decreasing `odrv0.axis0.motor.config.calibration_current` from its default value of 10A. - -In general, you need -```text -resistance_calib_max_voltage > calibration_current * phase_resistance -resistance_calib_max_voltage < 0.5 * vbus_voltage -``` - -* `ERROR_DRV_FAULT = 0x0008` - -The ODrive v3.4 is known to have a hardware issue whereby the motors would stop operating -when applying high currents to M0. The reported error of both motors in this case -is `ERROR_DRV_FAULT`. - -The conjecture is that the high switching current creates large ripples in the -power supply of the DRV8301 gate driver chips, thus tripping its under-voltage fault detection. - -To resolve this issue you can limit the M0 current to 40A. The lowest current at which the DRV fault was observed is 45A on one test motor and 50A on another test motor. Refer to [this post](https://discourse.odriverobotics.com/t/drv-fault-on-odrive-v3-4/558) for instructions for a hardware fix. - -* `ERROR_MODULATION_MAGNITUDE = 0x0080` - -The bus voltage was insufficent to push the requested current through the motor. -If you are getting this during motor calibration, make sure that `motor.config.resistance_calib_max_voltage` is no more than half your bus voltage. - -For gimbal motors, it is recommended to set the `motor.config.calibration_current` and `motor.config.current_lim` to half your bus voltage, or less. - -## Common Encoder Errors - -* `ERROR_CPR_OUT_OF_RANGE = 0x02` - -Confirm you have entered the correct count per rotation (CPR) for [your encoder](https://docs.odriverobotics.com/encoders). The ODrive uses your supplied value for the motor pole pairs to measure the CPR. So you should also double check this value. - -Note that the AMT encoders are configurable using the micro-switches on the encoder PCB and so you may need to check that these are in the right positions. If your encoder lists its pulse per rotation (PPR) multiply that number by four to get CPR. - -* `ERROR_NO_RESPONSE = 0x04` - -Confirm that your encoder is plugged into the right pins on the odrive board. - -* `ERROR_INDEX_NOT_FOUND_YET = 0x20` - -Check that your encoder is a model that has an index pulse. If your encoder does not have a wire connected to pin Z on your odrive then it does not output an index pulse. - -## Common Controller Errors - -* `ERROR_OVERSPEED = 0x01` - -Try increasing `.controller.config.vel_limit`. The default `vel_limit` of 20,000 encoder counts per second gives a motor speed of only ~146 RPM with the common CUI-AMT102 8192 count per rotation encoder. Note: Even if you do not commanded your motor to exceed `vel_limit` sudden changes in the load placed on a motor may cause this speed to be temporarily exceeded, resulting in this error. - -You can also try increasing `.controller.config.vel_limit_tolerance`. The default value of 1.2 means it will only allow a 20% violation of the speed limit. You can set the `vel_limit_tolerance` to 0 to disable the check altogether. +With this information you can look up the API documentation for your error(s): +* Axis error flags documented [here](api/odrive.axis.error). +* Motor error flags documented [here](api/odrive.motor.error). +* Encoder error flags documented [here](api/odrive.encoder.error). +* Controller error flags documented [here](api/odrive.controller.error). +* Sensorless estimator error flags documented [here](odrive.sensorlessestimator.error). ## USB Connectivity Issues @@ -113,7 +27,7 @@ You can also try increasing `.controller.config.vel_limit_tolerance`. The * **Linux**: Type `lsusb` to list all USB devices. Verify that your ODrive is listed. * **Linux**: Make sure you [set up your udev rules](getting-started#downloading-and-installing-tools) correctly. * **Windows**: Right-click on the start menu and open "Device Manager". Verify that your ODrive is listed. - * **Windows**: Use the [Zadig utility](http://zadig.akeo.ie/) to verify the driver is set to `libusb-win32`. + * **Windows**: Use the [Zadig utility](http://zadig.akeo.ie/) to verify the driver is set to `libusb-win32`. Note that there are two options listed in Zadig for Odrive: `ODrive 3.x Native Interface (Interface 2)` and `ODrive 3.x CDC Interface (Interface 0)`. Only the native interface should have `libusb-win32` while the CDC interface should use `WinUSB`. * Ensure that no other ODrive program is running * Run `odrivetools` with the `--verbose` option. * Run `PYUSB_DEBUG=debug odrivetools` to get even more log output. diff --git a/tools/.vscode/launch.json b/tools/.vscode/launch.json index a3b07eff..9a36a076 100644 --- a/tools/.vscode/launch.json +++ b/tools/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "python", "request": "launch", "stopOnEntry": true, - "pythonPath": "${config:python.pythonPath}", + "pythonPath": "${command:python.pythonPath}", "program": "${file}", "cwd": "${workspaceRoot}", "env": {}, diff --git a/tools/enums_template.j2 b/tools/enums_template.j2 new file mode 100644 index 00000000..bb20ca37 --- /dev/null +++ b/tools/enums_template.j2 @@ -0,0 +1,15 @@ + +# TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. +# To regenerate this file, nagivate to the top level of the ODrive repository and run: +# python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template tools/enums_template.j2 --output tools/odrive/enums.py + +[%- for _, enum in value_types.items() %] +[%- if enum.is_enum %] + +# [[enum.fullname]] +[%- for k, value in enum['values'].items() %] +[[(((enum.parent.name if enum.name in ['Error', 'Mode'] else '') + enum.name + k) | to_macro_case).ljust(40)]] = [% if enum.is_flags %]0x[['%08x' | format(value.value)]][% else %][[value.value]][% endif %] +[%- endfor %] +[%- endif %] +[%- endfor %] + diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py index c5f6812c..c836f39d 100644 --- a/tools/odrive/enums.py +++ b/tools/odrive/enums.py @@ -1,71 +1,147 @@ -# TODO: This is dangerous. Transmit as part of the JSON +# TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON. +# To regenerate this file, nagivate to the top level of the ODrive repository and run: +# python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template tools/enums_template.j2 --output tools/odrive/enums.py -AXIS_STATE_UNDEFINED = 0 -AXIS_STATE_IDLE = 1 -AXIS_STATE_STARTUP_SEQUENCE = 2 -AXIS_STATE_FULL_CALIBRATION_SEQUENCE = 3 -AXIS_STATE_MOTOR_CALIBRATION = 4 -AXIS_STATE_SENSORLESS_CONTROL = 5 -AXIS_STATE_ENCODER_INDEX_SEARCH = 6 -AXIS_STATE_ENCODER_OFFSET_CALIBRATION = 7 -AXIS_STATE_CLOSED_LOOP_CONTROL = 8 -AXIS_STATE_LOCKIN_SPIN = 9 -AXIS_STATE_ENCODER_DIR_FIND = 10 +# ODrive.Can.Protocol +PROTOCOL_SIMPLE = 0 -class errors: - class axis: - ERROR_NONE = 0x00 - ERROR_INVALID_STATE = 0x01 #') print('This will present you with all the properties that you can reference') print('') - print('For example: "odrv0.motor0.encoder.pos_estimate"') - print('will print the current encoder position on motor 0') - print('and "odrv0.motor0.pos_setpoint = 10000"') - print('will send motor0 to 10000') + print('For example: "odrv0.axis0.encoder.pos_estimate"') + print('will print the current encoder position on axis 0') + print('and "odrv0.axis0.controller.input_pos = 0.5"') + print('will send axis 0 to 0.5 turns') print('') @@ -77,7 +83,12 @@ def launch_shell(args, logger, app_shutdown_token): interactive_variables = { 'start_liveplotter': start_liveplotter, - 'dump_errors': dump_errors + 'dump_errors': dump_errors, + 'oscilloscope_dump': oscilloscope_dump, + 'BulkCapture': BulkCapture, + 'step_and_plot': step_and_plot, + 'calculate_thermistor_coeffs': calculate_thermistor_coeffs, + 'set_motor_thermistor_coeffs': set_motor_thermistor_coeffs } # Expose all enums from odrive.enums diff --git a/tools/odrive/tests/analog_input_test.py b/tools/odrive/tests/analog_input_test.py new file mode 100644 index 00000000..733f2a8d --- /dev/null +++ b/tools/odrive/tests/analog_input_test.py @@ -0,0 +1,113 @@ + +import test_runner + +import time +import math +import os +import numpy as np + +from odrive.enums import * +from test_runner import * + + +teensy_code_template = """ +void setup() { + analogWriteResolution(10); + // base clock of the PWM timer is 150MHz (on Teensy 4.0) + int freq = 150000000/1024; // ~146.5kHz PWM frequency + analogWriteFrequency({analog_out}, freq); + + // for filtering, assuming we have a 150 Ohm resistor, we need a capacitor of + // 1/(150000000/1024)*2*pi/150 = 2.85954744646751e-07 F, that's ~0.33uF + + //pinMode({lpf_enable}, OUTPUT); +} + +int i = 0; +void loop() { + i++; + i = i & 0x3ff; + if (digitalRead({analog_reset})) + i = 0; + analogWrite({analog_out}, i); + delay(1); +} +""" + + +class TestAnalogInput(): + """ + Verifies the Analog input. + + The Teensy generates a PWM signal with a duty cycle that follows a sawtooth signal + with a period of 1 second. The signal should be connected to the ODrive's + analog input through a low-pass-filter. + + ___ ___ + Teensy PWM ----|___|-------o---------|___|----- ODrive Analog Input + 150 Ohm | 150 Ohm + === + | 330nF + | + GND + + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for odrive_gpio_num, odrive_gpio in [(2, odrive.gpio3), (3, odrive.gpio4)]: + analog_out_options = [] + lpf_gpio = [gpio for lpf in testrig.get_connected_components(odrive_gpio, LowPassFilterComponent) + for gpio in testrig.get_connected_components(lpf.en, LinuxGpioComponent)] + for teensy_gpio in testrig.get_connected_components(odrive_gpio, TeensyGpio): + teensy = teensy_gpio.parent + analog_reset_options = [] + for gpio in teensy.gpios: + for local_gpio in testrig.get_connected_components(gpio, LinuxGpioComponent): + analog_reset_options.append((gpio, local_gpio)) + analog_out_options.append((teensy, teensy_gpio, analog_reset_options)) + yield (odrive, lpf_gpio, odrive_gpio_num, analog_out_options) + + + def run_test(self, odrive: ODriveComponent, lpf_enable: LinuxGpioComponent, analog_in_num: int, teensy: TeensyComponent, teensy_analog_out: Component, teensy_analog_reset: Component, analog_reset_gpio: LinuxGpioComponent, logger: Logger): + code = teensy_code_template.replace("{analog_out}", str(teensy_analog_out.num)).replace("{analog_reset}", str(teensy_analog_reset.num)) #.replace("lpf_enable", str(lpf_enable.num)) + teensy.compile_and_program(code) + analog_reset_gpio.config(output=True) + analog_reset_gpio.write(True) + lpf_enable.config(output=True) + lpf_enable.write(False) + + logger.debug("Set up analog input...") + + min_val = -20000 + max_val = 20000 + period = 1.025 # period in teensy code is 1s, but due to tiny overhead it's a bit longer + + analog_mapping = [ + None, #odrive.handle.config.gpio1_analog_mapping, + None, #odrive.handle.config.gpio2_analog_mapping, + odrive.handle.config.gpio3_analog_mapping, + odrive.handle.config.gpio4_analog_mapping, + None, #odrive.handle.config.gpio5_analog_mapping, + ][analog_in_num] + + odrive.unuse_gpios() + analog_mapping.endpoint = odrive.handle.axis0.controller._remote_attributes['input_pos'] + analog_mapping.min = min_val + analog_mapping.max = max_val + odrive.save_config_and_reboot() + + analog_reset_gpio.write(False) + data = record_log(lambda: [odrive.handle.axis0.controller.input_pos], duration=5.0) + + # Expect mean error to be at most 2% (of the full scale). + # Expect there to be less than 2% outliers, where an outlier is anything that is more than 5% (of full scale) away from the expected value. + full_range = abs(max_val - min_val) + slope, offset, fitted_curve = fit_sawtooth(data, min_val, max_val, sigma=30) + test_assert_eq(slope, (max_val - min_val) / period, accuracy=0.005) + test_curve_fit(data, fitted_curve, max_mean_err = full_range * 0.02, inlier_range = full_range * 0.05, max_outliers = len(data[:,0]) * 0.02) + + + +if __name__ == '__main__': + test_runner.run(TestAnalogInput()) diff --git a/tools/odrive/tests/calibration_test.py b/tools/odrive/tests/calibration_test.py new file mode 100644 index 00000000..4b9b6788 --- /dev/null +++ b/tools/odrive/tests/calibration_test.py @@ -0,0 +1,241 @@ + +import test_runner + +import time +from math import pi +import os + +from fibre.utils import Logger +from test_runner import * +from odrive.enums import * + + +class TestMotorCalibration(): + """ + Runs the motor calibration (phase inductance and phase resistance measurement) + and checks if the measurements match the expectation. + """ + + def get_test_cases(self, testrig: TestRig): + """Returns all axes that are connected to a motor, along with the corresponding motor(s)""" + for odrive in testrig.get_components(ODriveComponent): + for axis in odrive.axes: + for motor in testrig.get_connected_components(axis, MotorComponent): + yield (axis, motor) + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, logger: Logger): + # reset old calibration values + + if axis_ctx.handle.encoder.config.mode != ENCODER_MODE_INCREMENTAL: + axis_ctx.handle.encoder.config.mode = ENCODER_MODE_INCREMENTAL + axis_ctx.parent.save_config_and_reboot() + + axis_ctx.handle.motor.config.phase_resistance = 0.0 + axis_ctx.handle.motor.config.phase_inductance = 0.0 + axis_ctx.handle.motor.config.pre_calibrated = False + axis_ctx.handle.config.enable_watchdog = False + + axis_ctx.handle.clear_errors() + + # run calibration + request_state(axis_ctx, AXIS_STATE_MOTOR_CALIBRATION) + time.sleep(6) + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_no_error(axis_ctx) + + # check if measurements match expectation + test_assert_eq(axis_ctx.handle.motor.config.phase_resistance, float(motor_ctx.yaml['phase-resistance']), accuracy=0.2) + test_assert_eq(axis_ctx.handle.motor.config.phase_inductance, float(motor_ctx.yaml['phase-inductance']), accuracy=0.5) + test_assert_eq(axis_ctx.handle.motor.is_calibrated, True) + + +class TestDisconnectedMotorCalibration(): + """ + Tests if the motor calibration fails as expected if the phases are floating. + """ + + def get_test_cases(self, testrig: TestRig): + """Returns all axes that are disconnected""" + for odrive in testrig.get_components(ODriveComponent): + for axis in odrive.axes: + if axis.yaml == 'floating': + yield (axis,) + + def run_test(self, axis_ctx: ODriveAxisComponent, logger: Logger): + axis = axis_ctx.handle + + # reset old calibration values + axis_ctx.handle.motor.config.phase_resistance = 0.0 + axis_ctx.handle.motor.config.phase_inductance = 0.0 + axis_ctx.handle.motor.config.pre_calibrated = False + + axis_ctx.handle.clear_errors() + + # run test + request_state(axis_ctx, AXIS_STATE_MOTOR_CALIBRATION) + time.sleep(6) + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_eq(axis_ctx.handle.error, AXIS_ERROR_MOTOR_FAILED) + test_assert_eq(axis_ctx.handle.motor.error, MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE) + + +class TestEncoderDirFind(): + """ + Runs the encoder index search. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for num in range(2): + encoders = testrig.get_connected_components({ + 'a': (odrive.encoders[num].a, False), + 'b': (odrive.encoders[num].b, False) + }, EncoderComponent) + motors = testrig.get_connected_components(odrive.axes[num], MotorComponent) + + for motor, encoder in itertools.product(motors, encoders): + if encoder.impl in testrig.get_connected_components(motor): + yield (odrive.axes[num], motor, encoder) + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + axis = axis_ctx.handle + time.sleep(1.0) # wait for PLLs to stabilize + + # Set motor calibration values + axis_ctx.handle.motor.config.phase_resistance = float(motor_ctx.yaml['phase-resistance']) + axis_ctx.handle.motor.config.phase_inductance = float(motor_ctx.yaml['phase-inductance']) + axis_ctx.handle.motor.config.pre_calibrated = True + + # Set calibration settings + axis_ctx.handle.motor.config.direction = 0 + axis_ctx.handle.config.calibration_lockin.vel = 12.566 # 2 electrical revolutions per second + + axis_ctx.handle.clear_errors() + + # run test + request_state(axis_ctx, AXIS_STATE_ENCODER_DIR_FIND) + + time.sleep(4) # actual calibration takes 3 seconds + + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_no_error(axis_ctx) + + test_assert_eq(axis_ctx.handle.motor.config.direction in [-1, 1], True) + + +class TestEncoderOffsetCalibration(): + """ + Runs the encoder index search. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for num in range(2): + encoders = testrig.get_connected_components({ + 'a': (odrive.encoders[num].a, False), + 'b': (odrive.encoders[num].b, False) + }, EncoderComponent) + motors = testrig.get_connected_components(odrive.axes[num], MotorComponent) + + for motor, encoder in itertools.product(motors, encoders): + if encoder.impl in testrig.get_connected_components(motor): + yield (odrive.axes[num], motor, encoder) + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + axis = axis_ctx.handle + time.sleep(1.0) # wait for PLLs to stabilize + + # Set motor calibration values + axis_ctx.handle.motor.config.phase_resistance = float(motor_ctx.yaml['phase-resistance']) + axis_ctx.handle.motor.config.phase_inductance = float(motor_ctx.yaml['phase-inductance']) + axis_ctx.handle.motor.config.pre_calibrated = True + + # Set calibration settings + axis_ctx.handle.motor.config.direction = 0 + axis_ctx.handle.encoder.config.use_index = False + axis_ctx.handle.encoder.config.calib_scan_omega = 12.566 # 2 electrical revolutions per second + axis_ctx.handle.encoder.config.calib_scan_distance = 50.265 # 8 revolutions + + axis_ctx.handle.clear_errors() + + # run test + request_state(axis_ctx, AXIS_STATE_ENCODER_OFFSET_CALIBRATION) + + time.sleep(9) # actual calibration takes 8 seconds + + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_no_error(axis_ctx) + + test_assert_eq(axis_ctx.handle.encoder.is_ready, True) + test_assert_eq(axis_ctx.handle.motor.config.direction in [-1, 1], True) + + +class TestEncoderIndexSearch(): + """ + Runs the encoder index search. + The index pin is triggered manually after three seconds from the testbench + host's GPIO. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for num in range(2): + encoders = testrig.get_connected_components({ + 'a': (odrive.encoders[num].a, False), + 'b': (odrive.encoders[num].b, False) + }, EncoderComponent) + motors = testrig.get_connected_components(odrive.axes[num], MotorComponent) + z_gpio = list(testrig.get_connected_components((odrive.encoders[num].z, False), LinuxGpioComponent)) + + for motor, encoder in itertools.product(motors, encoders): + if encoder.impl in testrig.get_connected_components(motor): + yield (odrive.axes[num], motor, encoder, z_gpio) + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, z_gpio: LinuxGpioComponent, logger: Logger): + axis = axis_ctx.handle + cpr = int(enc_ctx.yaml['cpr']) + + z_gpio.config(output=True) + z_gpio.write(False) + + time.sleep(1.0) # wait for PLLs to stabilize + + # Set motor calibration values + axis_ctx.handle.motor.config.phase_resistance = float(motor_ctx.yaml['phase-resistance']) + axis_ctx.handle.motor.config.phase_inductance = float(motor_ctx.yaml['phase-inductance']) + axis_ctx.handle.motor.config.pre_calibrated = True + + # Set calibration settings + axis_ctx.handle.config.calibration_lockin.vel = 12.566 # 2 electrical revolutions per second + + axis_ctx.handle.clear_errors() + + # run test + request_state(axis_ctx, AXIS_STATE_ENCODER_INDEX_SEARCH) + + time.sleep(3) + + test_assert_eq(axis_ctx.handle.encoder.index_found, False) + time.sleep(0.1) + z_gpio.write(True) + test_assert_eq(axis_ctx.handle.encoder.index_found, True) + z_gpio.write(False) + + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_no_error(axis_ctx) + + test_assert_eq(axis_ctx.handle.encoder.shadow_count, 0.0, range=50) + test_assert_eq(modpm(axis_ctx.handle.encoder.count_in_cpr, cpr), 0.0, range=50) + test_assert_eq(axis_ctx.handle.encoder.pos_estimate, 0.0, range=50) + test_assert_eq(modpm(axis_ctx.handle.encoder.pos_cpr, cpr), 0.0, range=50) + test_assert_eq(axis_ctx.handle.encoder.pos_abs, 0.0, range=50) + + +if __name__ == '__main__': + test_runner.run([ + TestMotorCalibration(), + TestDisconnectedMotorCalibration(), + TestEncoderDirFind(), + TestEncoderOffsetCalibration(), + TestEncoderIndexSearch() + ]) diff --git a/tools/odrive/tests/can_test.py b/tools/odrive/tests/can_test.py new file mode 100644 index 00000000..3195a806 --- /dev/null +++ b/tools/odrive/tests/can_test.py @@ -0,0 +1,235 @@ + +import test_runner + +import struct +import can +import asyncio +import time +import math + +from fibre.utils import Logger +from odrive.enums import * +from test_runner import * + +# Each argument is described as tuple (name, format, scale). +# Struct format codes: https://docs.python.org/2/library/struct.html +command_set = { + 'heartbeat': (0x001, [('error', 'I', 1), ('current_state', 'I', 1)]), # tested + 'estop': (0x002, []), # tested + 'get_motor_error': (0x003, [('motor_error', 'I', 1)]), # untested + 'get_encoder_error': (0x004, [('encoder_error', 'I', 1)]), # untested + 'get_sensorless_error': (0x005, [('sensorless_error', 'I', 1)]), # untested + 'set_node_id': (0x006, [('node_id', 'I', 1)]), # tested + 'set_requested_state': (0x007, [('requested_state', 'I', 1)]), # tested + # 0x008 not yet implemented + 'get_encoder_estimates': (0x009, [('encoder_pos_estimate', 'f', 1), ('encoder_vel_estimate', 'f', 1)]), # partially tested + 'get_encoder_count': (0x00a, [('encoder_shadow_count', 'i', 1), ('encoder_count', 'i', 1)]), # partially tested + 'set_controller_modes': (0x00b, [('control_mode', 'i', 1), ('input_mode', 'i', 1)]), # tested + 'set_input_pos': (0x00c, [('input_pos', 'f', 1), ('vel_ff', 'h', 0.001), ('torque_ff', 'h', 0.001)]), # tested + 'set_input_vel': (0x00d, [('input_vel', 'f', 1), ('torque_ff', 'f', 1)]), # tested + 'set_input_torque': (0x00e, [('input_torque', 'f', 1)]), # tested + 'set_velocity_limit': (0x00f, [('velocity_limit', 'f', 1)]), # tested + 'start_anticogging': (0x010, []), # untested + 'set_traj_vel_limit': (0x011, [('traj_vel_limit', 'f', 1)]), # tested + 'set_traj_accel_limits': (0x012, [('traj_accel_limit', 'f', 1), ('traj_decel_limit', 'f', 1)]), # tested + 'set_traj_inertia': (0x013, [('inertia', 'f', 1)]), # tested + 'get_iq': (0x014, [('iq_setpoint', 'f', 1), ('iq_measured', 'f', 1)]), # untested + 'get_sensorless_estimates': (0x015, [('sensorless_pos_estimate', 'f', 1), ('sensorless_vel_estimate', 'f', 1)]), # untested + 'reboot': (0x016, []), # tested + 'get_vbus_voltage': (0x017, [('vbus_voltage', 'f', 1)]), # tested + 'clear_errors': (0x018, []), # partially tested +} + +def command(bus, node_id_, extended_id, cmd_name, **kwargs): + cmd_spec = command_set[cmd_name] + cmd_id = cmd_spec[0] + fmt = '<' + ''.join([f for (n, f, s) in cmd_spec[1]]) # all little endian + + if (sorted([n for (n, f, s) in cmd_spec[1]]) != sorted(kwargs.keys())): + raise Exception("expected arguments: " + str([n for (n, f, s) in cmd_spec[1]])) + + fields = [((kwargs[n] / s) if f == 'f' else int(kwargs[n] / s)) for (n, f, s) in cmd_spec[1]] + data = struct.pack(fmt, *fields) + msg = can.Message(arbitration_id=((node_id_ << 5) | cmd_id), extended_id=extended_id, data=data) + bus.send(msg) + +async def record_messages(bus, node_id, extended_id, cmd_name, timeout = 5.0): + """ + Returns an async generator that yields a dictionary for each CAN message that + is received, provided that the CAN ID matches the expected value. + """ + + cmd_spec = command_set[cmd_name] + cmd_id = cmd_spec[0] + fmt = '<' + ''.join([f for (n, f, s) in cmd_spec[1]]) # all little endian + + reader = can.AsyncBufferedReader() + notifier = can.Notifier(bus, [reader], timeout = timeout, loop = asyncio.get_event_loop()) + + try: + # The timeout in can.Notifier only triggers if no new messages are received at all, + # so we need a second monitoring method. + start = time.monotonic() + while True: + msg = await reader.get_message() + if ((msg.arbitration_id == ((node_id << 5) | cmd_id)) and (msg.is_extended_id == extended_id) and not msg.is_remote_frame): + fields = struct.unpack(fmt, msg.data[:(struct.calcsize(fmt))]) + res = {n: (fields[i] * s) for (i, (n, f, s)) in enumerate(cmd_spec[1])} + res['t'] = time.monotonic() + yield res + if (time.monotonic() - start) > timeout: + break + finally: + notifier.stop() + +async def request(bus, node_id, extended_id, cmd_name, timeout = 1.0): + cmd_spec = command_set[cmd_name] + cmd_id = cmd_spec[0] + + msg_generator = record_messages(bus, node_id, extended_id, cmd_name, timeout) + + msg = can.Message(arbitration_id=((node_id << 5) | cmd_id), extended_id=extended_id, data=[], is_remote_frame=True) + bus.send(msg) + + async for msg in msg_generator: + return msg + + raise TimeoutError() + +async def get_all(async_iterator): + return [x async for x in async_iterator] + + +class TestSimpleCAN(): + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + can_interfaces = list(testrig.get_connected_components(odrive.can, CanInterfaceComponent)) + yield (odrive, can_interfaces, 0, False) # standard ID + yield (odrive, can_interfaces, 0xfedcba, True) # extended ID + + def run_test(self, odrive: ODriveComponent, canbus: CanInterfaceComponent, node_id: int, extended_id: bool, logger: Logger): + + # make sure no gpio input is overwriting our values + odrive.unuse_gpios() + + axis = odrive.handle.axis0 + axis.config.enable_watchdog = False + axis.clear_errors() + axis.config.can_node_id = node_id + axis.config.can_node_id_extended = extended_id + time.sleep(0.1) + + def my_cmd(cmd_name, **kwargs): command(canbus.handle, node_id, extended_id, cmd_name, **kwargs) + def my_req(cmd_name, **kwargs): return asyncio.run(request(canbus.handle, node_id, extended_id, cmd_name, **kwargs)) + def fence(): my_req('get_vbus_voltage') # fence to ensure the CAN command was sent + + test_assert_eq(my_req('get_vbus_voltage')['vbus_voltage'], odrive.handle.vbus_voltage, accuracy=0.01) + + my_cmd('set_node_id', node_id=node_id+20) + asyncio.run(request(canbus.handle, node_id+20, extended_id, 'get_vbus_voltage')) + test_assert_eq(axis.config.can_node_id, node_id+20) + + # Reset node ID to default value + command(canbus.handle, node_id+20, extended_id, 'set_node_id', node_id=node_id) + fence() + test_assert_eq(axis.config.can_node_id, node_id) + + # Check that extended node IDs are not carelessly projected to 6-bit IDs + extended_id = not extended_id + my_cmd('estop') # should not be accepted + extended_id = not extended_id + fence() + test_assert_eq(axis.error, AXIS_ERROR_NONE) + + axis.encoder.set_linear_count(123) + test_assert_eq(my_req('get_encoder_estimates')['encoder_pos_estimate'], 123.0 / axis.encoder.config.cpr, accuracy=0.01) + test_assert_eq(my_req('get_encoder_count')['encoder_shadow_count'], 123.0, accuracy=0.01) + + my_cmd('clear_errors') + fence() + test_assert_eq(axis.error, 0) + + my_cmd('estop') + fence() + test_assert_eq(axis.error, AXIS_ERROR_ESTOP_REQUESTED) + + my_cmd('set_requested_state', requested_state=42) # illegal state - should assert axis error + fence() + test_assert_eq(axis.current_state, 1) # idle + test_assert_eq(axis.error, AXIS_ERROR_ESTOP_REQUESTED | AXIS_ERROR_INVALID_STATE) + + my_cmd('clear_errors') + fence() + test_assert_eq(axis.error, 0) + + my_cmd('set_controller_modes', control_mode=1, input_mode=5) # current conrol, traprzoidal trajectory + fence() + test_assert_eq(axis.controller.config.control_mode, 1) + test_assert_eq(axis.controller.config.input_mode, 5) + + # Reset to safe values + my_cmd('set_controller_modes', control_mode=3, input_mode=1) # position control, passthrough + fence() + test_assert_eq(axis.controller.config.control_mode, 3) + test_assert_eq(axis.controller.config.input_mode, 1) + + axis.controller.input_pos = 1234 + axis.controller.input_vel = 1234 + axis.controller.input_torque = 1234 + my_cmd('set_input_pos', input_pos=1.23, vel_ff=1.2, torque_ff=3.4) + fence() + test_assert_eq(axis.controller.input_pos, 1.23, range=0.1) + test_assert_eq(axis.controller.input_vel, 1.2, range=0.01) + test_assert_eq(axis.controller.input_torque, 3.4, range=0.001) + + axis.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL + my_cmd('set_input_vel', input_vel=-10.5, torque_ff=0.1234) + fence() + test_assert_eq(axis.controller.input_vel, -10.5, range=0.01) + test_assert_eq(axis.controller.input_torque, 0.1234, range=0.01) + + axis.controller.config.control_mode = CONTROL_MODE_TORQUE_CONTROL + my_cmd('set_input_torque', input_torque=0.1) + fence() + test_assert_eq(axis.controller.input_torque, 0.1, range=0.01) + + my_cmd('set_velocity_limit', velocity_limit=2.345678) + fence() + test_assert_eq(axis.controller.config.vel_limit, 2.345678, range=0.001) + + my_cmd('set_traj_vel_limit', traj_vel_limit=123.456) + fence() + test_assert_eq(axis.trap_traj.config.vel_limit, 123.456, range=0.0001) + + my_cmd('set_traj_accel_limits', traj_accel_limit=98.231, traj_decel_limit=-12.234) + fence() + test_assert_eq(axis.trap_traj.config.accel_limit, 98.231, range=0.0001) + test_assert_eq(axis.trap_traj.config.decel_limit, -12.234, range=0.0001) + + my_cmd('set_traj_inertia', inertia=55.086) + fence() + test_assert_eq(axis.controller.config.inertia, 55.086, range=0.0001) + + # any CAN cmd will feed the watchdog + test_watchdog(axis, lambda: my_cmd('set_input_torque', input_torque=0.0), logger) + + logger.debug('testing heartbeat...') + # note that this will include the heartbeats that were received during the + # watchdog test (which takes 4.8s). + heartbeats = asyncio.run(get_all(record_messages(canbus.handle, node_id, extended_id, 'heartbeat', timeout = 1.0))) + test_assert_eq(len(heartbeats), 5.8 / 0.1, accuracy=0.05) + test_assert_eq([msg['error'] for msg in heartbeats[0:35]], [0] * 35) # before watchdog expiry + test_assert_eq([msg['error'] for msg in heartbeats[-10:]], [AXIS_ERROR_WATCHDOG_TIMER_EXPIRED] * 10) # after watchdog expiry + test_assert_eq([msg['current_state'] for msg in heartbeats], [1] * len(heartbeats)) + + logger.debug('testing reboot...') + my_cmd('reboot') + time.sleep(0.5) + if len(odrive.handle._remote_attributes) != 0: + raise TestFailed("device didn't seem to reboot") + odrive.handle = None + time.sleep(2.0) + odrive.prepare(logger) + +if __name__ == '__main__': + test_runner.run(TestSimpleCAN()) diff --git a/tools/odrive/tests/closed_loop_test.py b/tools/odrive/tests/closed_loop_test.py new file mode 100644 index 00000000..263c8181 --- /dev/null +++ b/tools/odrive/tests/closed_loop_test.py @@ -0,0 +1,391 @@ + +import test_runner + +import time +from math import pi, inf +import os + +from fibre.utils import Logger +from test_runner import * +from odrive.enums import * + + +class TestClosedLoopControlBase(): + """ + Base class for close loop control tests. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for num in range(2): + encoders = testrig.get_connected_components({ + 'a': (odrive.encoders[num].a, False), + 'b': (odrive.encoders[num].b, False) + }, EncoderComponent) + motors = testrig.get_connected_components(odrive.axes[num], MotorComponent) + + for motor, encoder in itertools.product(motors, encoders): + if encoder.impl in testrig.get_connected_components(motor): + yield (odrive.axes[num], motor, encoder) + + def prepare(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + # Make sure there are no funny configurations active + logger.debug('Setting up clean configuration...') + axis_ctx.parent.erase_config_and_reboot() + + # Set motor calibration values + axis_ctx.handle.motor.config.phase_resistance = float(motor_ctx.yaml['phase-resistance']) + axis_ctx.handle.motor.config.phase_inductance = float(motor_ctx.yaml['phase-inductance']) + axis_ctx.handle.motor.config.pre_calibrated = True + + # Set calibration settings + axis_ctx.handle.motor.config.direction = 0 + axis_ctx.handle.encoder.config.use_index = False + axis_ctx.handle.encoder.config.calib_scan_omega = 12.566 # 2 electrical revolutions per second + axis_ctx.handle.encoder.config.calib_scan_distance = 50.265 # 8 revolutions + axis_ctx.handle.encoder.config.bandwidth = 1000 + + + axis_ctx.handle.clear_errors() + + logger.debug('Calibrating encoder offset...') + request_state(axis_ctx, AXIS_STATE_ENCODER_OFFSET_CALIBRATION) + + time.sleep(9) # actual calibration takes 8 seconds + + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_no_error(axis_ctx) + + + # Return a context that can be used in a with-statement. + class safe_terminator(): + def __enter__(self): + pass + def __exit__(self, exc_type, exc_val, exc_tb): + logger.debug('clearing config...') + axis_ctx.handle.requested_state = AXIS_STATE_IDLE + time.sleep(0.005) + axis_ctx.parent.erase_config_and_reboot() + return safe_terminator() + + +class TestClosedLoopControl(TestClosedLoopControlBase): + """ + Tests position and velocity control + """ + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + with self.prepare(axis_ctx, motor_ctx, enc_ctx, logger): + nominal_rps = 1.0 + nominal_vel = nominal_rps + logger.debug(f'Testing closed loop velocity control at {nominal_rps} rounds/s...') + + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL + axis_ctx.handle.controller.config.input_mode = INPUT_MODE_PASSTHROUGH + axis_ctx.handle.controller.input_vel = 0 + + request_state(axis_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) + axis_ctx.handle.controller.input_vel = nominal_vel + + data = record_log(lambda: [axis_ctx.handle.encoder.vel_estimate, axis_ctx.handle.encoder.pos_estimate], duration=5.0) + + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_CLOSED_LOOP_CONTROL) + test_assert_no_error(axis_ctx) + request_state(axis_ctx, AXIS_STATE_IDLE) + + # encoder.vel_estimate + slope, offset, fitted_curve = fit_line(data[:,(0,1)]) + test_assert_eq(slope, 0.0, range = nominal_vel * 0.02) + test_assert_eq(offset, nominal_vel, accuracy = 0.05) + test_curve_fit(data[:,(0,1)], fitted_curve, max_mean_err = nominal_vel * 0.3, inlier_range = nominal_vel * 0.5, max_outliers = len(data[:,0]) * 0.1) + + # encoder.pos_estimate + slope, offset, fitted_curve = fit_line(data[:,(0,2)]) + test_assert_eq(slope, nominal_vel, accuracy = 0.01) + test_curve_fit(data[:,(0,2)], fitted_curve, max_mean_err = nominal_vel * 0.01, inlier_range = nominal_vel * 0.1, max_outliers = len(data[:,0]) * 0.01) + + + logger.debug(f'Testing closed loop position control...') + + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL + axis_ctx.handle.controller.input_pos = 0 + axis_ctx.handle.controller.config.vel_limit = 5.0 # max 5 rps + axis_ctx.handle.encoder.set_linear_count(0) + + request_state(axis_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) + + # Test small position changes + test_pos = 5000 / float(enc_ctx.yaml['cpr']) + axis_ctx.handle.controller.input_pos = test_pos + time.sleep(0.3) + test_assert_no_error(axis_ctx) + test_assert_eq(axis_ctx.handle.encoder.pos_estimate, test_pos, range=0.4*test_pos) # large range needed because of cogging torque + axis_ctx.handle.controller.input_pos = -1 * test_pos + time.sleep(0.3) + test_assert_no_error(axis_ctx) + test_assert_eq(axis_ctx.handle.encoder.pos_estimate, -1 * test_pos, range=0.4*test_pos) + + axis_ctx.handle.controller.input_pos = 0 + time.sleep(0.3) + + nominal_vel = 5.0 + axis_ctx.handle.controller.input_pos = nominal_vel * 2.0 # 10 turns (takes 2 seconds) + + # Test large position change with bounded velocity + data = record_log(lambda: [axis_ctx.handle.encoder.vel_estimate, axis_ctx.handle.encoder.pos_estimate], duration=4.0) + + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_CLOSED_LOOP_CONTROL) + test_assert_no_error(axis_ctx) + request_state(axis_ctx, AXIS_STATE_IDLE) + + data_motion = data[data[:,0] < 1.9] + data_still = data[data[:,0] > 2.1] + + # encoder.vel_estimate + slope, offset, fitted_curve = fit_line(data_motion[:,(0,1)]) + test_assert_eq(slope, 0.0, range = nominal_vel * 0.05) + test_assert_eq(offset, nominal_vel, accuracy = 0.05) + test_curve_fit(data_motion[:,(0,1)], fitted_curve, max_mean_err = nominal_vel * 0.05, inlier_range = nominal_vel * 0.1, max_outliers = len(data[:,0]) * 0.01) + + # encoder.pos_estimate + slope, offset, fitted_curve = fit_line(data_motion[:,(0,2)]) + test_assert_eq(slope, nominal_vel, accuracy = 0.01) + test_curve_fit(data_motion[:,(0,2)], fitted_curve, max_mean_err = nominal_vel * 0.01, inlier_range = nominal_vel * 0.1, max_outliers = len(data[:,0]) * 0.01) + + # encoder.vel_estimate + slope, offset, fitted_curve = fit_line(data_still[:,(0,1)]) + test_assert_eq(slope, 0.0, range = nominal_vel * 0.05) + test_assert_eq(offset, 0.0, range = nominal_vel * 0.05) + test_curve_fit(data_still[:,(0,1)], fitted_curve, max_mean_err = nominal_vel * 0.05, inlier_range = nominal_vel * 0.1, max_outliers = len(data[:,0]) * 0.01) + + # encoder.pos_estimate + slope, offset, fitted_curve = fit_line(data_still[:,(0,2)]) + test_assert_eq(slope, 0.0, range = nominal_vel * 0.05) + test_assert_eq(offset, nominal_vel*2, range = nominal_vel * 0.02) + test_curve_fit(data_still[:,(0,2)], fitted_curve, max_mean_err = nominal_vel * 0.01, inlier_range = nominal_vel * 0.01, max_outliers = len(data[:,0]) * 0.01) + + +class TestRegenProtection(TestClosedLoopControlBase): + """ + Tries to brake with a disabled brake resistor. + This should result in a low level error disabling all power outputs. + + Note: If this test fails then try to run it at a DC voltage of 24V. + Ibus seems to be more noisy/sensitive at lower DC voltages. + """ + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + with self.prepare(axis_ctx, motor_ctx, enc_ctx, logger): + nominal_rps = 15.0 + nominal_vel = nominal_rps + max_current = 30.0 + + # Accept a bit of noise on Ibus + axis_ctx.parent.handle.config.dc_max_negative_current = -0.2 + + logger.debug(f'Brake control test from {nominal_rps} rounds/s...') + + axis_ctx.handle.controller.config.vel_limit = 25.0 # max 15 rps + axis_ctx.handle.motor.config.current_lim = max_current + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL + axis_ctx.handle.controller.config.input_mode = INPUT_MODE_PASSTHROUGH + + request_state(axis_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) + # accelerate... + axis_ctx.handle.controller.input_vel = nominal_vel + time.sleep(1.0) + test_assert_no_error(axis_ctx) + + # ... and brake + axis_ctx.handle.controller.input_vel = 0 + time.sleep(1.0) + test_assert_no_error(axis_ctx) + + # once more, but this time without brake resistor + axis_ctx.parent.handle.config.brake_resistance = 0 + # accelerate... + axis_ctx.handle.controller.input_vel = nominal_vel + time.sleep(1.0) + test_assert_no_error(axis_ctx) + + # ... and brake + axis_ctx.handle.controller.input_vel = 0 # this should fail almost instantaneously + time.sleep(0.1) + test_assert_eq(axis_ctx.handle.error, AXIS_ERROR_MOTOR_DISARMED | AXIS_ERROR_BRAKE_RESISTOR_DISARMED) + test_assert_eq(axis_ctx.handle.motor.error, MOTOR_ERROR_DC_BUS_OVER_REGEN_CURRENT) + + +class TestVelLimitInTorqueControl(TestClosedLoopControlBase): + """ + Ensures that the current setpoint in torque control is always within the + parallelogram that arises from -Ilim, +Ilim, vel_limit and vel_gain. + """ + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + with self.prepare(axis_ctx, motor_ctx, enc_ctx, logger): + max_rps = 20.0 + max_vel = max_rps + absolute_max_vel = max_vel * 1.2 + max_current = 30.0 + torque_constant = 0.0305 #correct for 5065 motor + + axis_ctx.handle.controller.config.vel_gain /= 10 # reduce the slope to make it easier to see what's going on + vel_gain = axis_ctx.handle.controller.config.vel_gain + direction = axis_ctx.handle.motor.config.direction + logger.debug(f'vel gain is {vel_gain}') + + axis_ctx.handle.controller.config.vel_limit = max_vel + axis_ctx.handle.controller.config.vel_limit_tolerance = inf # disable hard limit on velocity + axis_ctx.handle.motor.config.current_lim = max_current + axis_ctx.handle.motor.config.torque_constant = torque_constant + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_TORQUE_CONTROL + + # Returns the expected limited setpoint for a given velocity and current + def get_expected_setpoint(input_setpoint, velocity): + return clamp(clamp(input_setpoint / torque_constant, (velocity + max_vel) * -vel_gain / torque_constant, (velocity - max_vel) * -vel_gain / torque_constant), -max_current, max_current) * direction + + def data_getter(): + # sample velocity twice to avoid systematic bias + velocity0 = axis_ctx.handle.encoder.vel_estimate + current_setpoint = axis_ctx.handle.motor.current_control.Iq_setpoint + velocity1 = axis_ctx.handle.encoder.vel_estimate + velocity = ((velocity0 + velocity1) / 2) + # Abort immediately if the absolute limits are exceeded + test_assert_within(current_setpoint, -max_current, max_current) + test_assert_within(velocity, -absolute_max_vel, absolute_max_vel) + return input_torque, velocity, current_setpoint, get_expected_setpoint(input_torque, velocity) + + axis_ctx.handle.controller.input_torque = input_torque = 0.0 + request_state(axis_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) + + # Move the system around its operating envelope + axis_ctx.handle.controller.input_torque = input_torque = 2.0 * torque_constant + dataA = record_log(data_getter, duration=1.0) + axis_ctx.handle.controller.input_torque = input_torque = -2.0 * torque_constant + dataA = np.concatenate([dataA, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_torque = input_torque = 4.0 * torque_constant + dataA = np.concatenate([dataA, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_torque = input_torque = -4.0 * torque_constant + dataA = np.concatenate([dataA, record_log(data_getter, duration=1.0)]) + + # Shrink the operating envelope while motor is moving faster than the envelope allows + max_rps = 5.0 + max_vel = max_rps + axis_ctx.handle.controller.config.vel_limit = max_vel + + # Move the system around its operating envelope + axis_ctx.handle.controller.input_torque = input_torque = 2.0 * torque_constant + dataB = record_log(data_getter, duration=1.0) + axis_ctx.handle.controller.input_torque = input_torque = -2.0 * torque_constant + dataB = np.concatenate([dataB, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_torque = input_torque = 4.0 * torque_constant + dataB = np.concatenate([dataB, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_torque = input_torque = -4.0 * torque_constant + dataB = np.concatenate([dataB, record_log(data_getter, duration=1.0)]) + + # Try the shrink maneuver again at positive velocity + axis_ctx.handle.controller.config.vel_limit = 20.0 + axis_ctx.handle.controller.input_torque = 4.0 * torque_constant + time.sleep(0.5) + axis_ctx.handle.controller.config.vel_limit = max_vel + + axis_ctx.handle.controller.input_torque = input_torque = 2.0 * torque_constant + dataB = np.concatenate([dataB, record_log(data_getter, duration=1.0)]) + + test_assert_no_error(axis_ctx) + + axis_ctx.handle.requested_state=1 + + test_curve_fit(dataA[:,(0,3)], dataA[:,4], max_mean_err=0.02, inlier_range=0.05, max_outliers=len(dataA[:,0]*0.01)) + test_curve_fit(dataB[:,(0,3)], dataB[:,4], max_mean_err=0.1, inlier_range=0.2, max_outliers=len(dataB[:,0])*0.01) + +class TestTorqueLimit(TestClosedLoopControlBase): + """ + Checks that the torque limit is respected in position, velocity, and torque control modes + """ + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + with self.prepare(axis_ctx, motor_ctx, enc_ctx, logger): + max_rps = 15.0 + max_vel = max_rps + max_current = 30.0 + max_torque = 0.1 # must be less than max_current * torque_constant. + torque_constant = axis_ctx.handle.motor.config.torque_constant + + test_pos = 5 + test_vel = 10 + test_torque = 0.5 + + axis_ctx.handle.controller.config.vel_limit = max_vel + axis_ctx.handle.motor.config.current_lim = max_current + axis_ctx.handle.motor.config.torque_lim = inf #disable torque limit + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL + + def data_getter(): + current_setpoint = axis_ctx.handle.motor.current_control.Iq_setpoint + torque_setpoint = current_setpoint * torque_constant + torque_limit = axis_ctx.handle.motor.config.torque_lim + # Abort immediately if the absolute limits are exceeded + test_assert_within(current_setpoint, -max_current, max_current) + test_assert_within(torque_setpoint, -torque_limit, torque_limit) + return max_current, current_setpoint, torque_limit, torque_setpoint + + # begin test + axis_ctx.handle.motor.config.torque_lim = max_torque + request_state(axis_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) + + # step input positions + logger.debug('input_pos step test') + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL + axis_ctx.handle.controller.input_pos = test_pos + dataPos = record_log(data_getter, duration=1.0) + axis_ctx.handle.controller.input_pos = -test_pos + dataPos = np.concatenate([dataPos, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_pos = test_pos + dataPos = np.concatenate([dataPos, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_pos = -test_pos + dataPos = np.concatenate([dataPos, record_log(data_getter, duration=1.0)]) + time.sleep(0.5) + + test_assert_no_error(axis_ctx) + + # step input velocities + logger.debug('input_vel step test') + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL + axis_ctx.handle.controller.input_vel = test_vel + dataVel = record_log(data_getter, duration=1.0) + axis_ctx.handle.controller.input_vel = -test_vel + dataVel = np.concatenate([dataVel, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_vel = test_vel + dataVel = np.concatenate([dataVel, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_vel = -test_vel + dataVel = np.concatenate([dataVel, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_vel = 0 + time.sleep(0.5) + + # step input torques + logger.debug('input_torque step test') + axis_ctx.handle.controller.config.control_mode = CONTROL_MODE_TORQUE_CONTROL + axis_ctx.handle.controller.input_torque = test_torque + dataTq = record_log(data_getter, duration=1.0) + axis_ctx.handle.controller.input_torque = -test_torque + dataTq = np.concatenate([dataTq, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_torque = test_torque + dataTq = np.concatenate([dataTq, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_torque = -test_torque + dataTq = np.concatenate([dataTq, record_log(data_getter, duration=1.0)]) + axis_ctx.handle.controller.input_torque = 0 + time.sleep(0.5) + + # did we pass? + + test_assert_no_error(axis_ctx) + + axis_ctx.handle.requested_state=1 + +if __name__ == '__main__': + test_runner.run([ + TestClosedLoopControl(), + TestRegenProtection(), + TestVelLimitInTorqueControl(), + TestTorqueLimit() + ]) diff --git a/tools/odrive/tests/encoder_test.py b/tools/odrive/tests/encoder_test.py new file mode 100644 index 00000000..d94059d5 --- /dev/null +++ b/tools/odrive/tests/encoder_test.py @@ -0,0 +1,512 @@ + +import test_runner + +import time +from math import pi +import os + +from fibre.utils import Logger +from odrive.enums import * +from test_runner import * + + +class TestEncoderBase(): + """ + Base class for encoder tests. + TODO: incremental encoder doesn't use this yet. + + All encoder tests expect the encoder to run at a constant velocity. + This can be achieved by generating an encoder signal with a Teensy. + + During 5 seconds, several variables are recorded and then compared against + the expected waveform. This is either a straight line, a sawtooth function + or a constant. + """ + + def run_generic_encoder_test(self, encoder, true_cpr, true_rps, noise=1): + encoder.config.cpr = true_cpr + true_cps = true_cpr * true_rps + + encoder.set_linear_count(0) # prevent numerical errors + data = record_log(lambda: [ + encoder.shadow_count, + encoder.count_in_cpr, + encoder.phase, + encoder.pos_estimate_counts, + encoder.pos_cpr_counts, + encoder.vel_estimate_counts, + ], duration=5.0) + + short_period = (abs(1 / true_rps) < 5.0) + reverse = (true_rps < 0) + + # encoder.shadow_count + slope, offset, fitted_curve = fit_line(data[:,(0,1)]) + test_assert_eq(slope, true_cps, accuracy=0.005) + test_curve_fit(data[:,(0,1)], fitted_curve, max_mean_err = true_cpr * 0.02, inlier_range = true_cpr * 0.02, max_outliers = len(data[:,0]) * 0.02) + + # encoder.count_in_cpr + slope, offset, fitted_curve = fit_sawtooth(data[:,(0,2)], true_cpr if reverse else 0, 0 if reverse else true_cpr) + test_assert_eq(slope, true_cps, accuracy=0.005) + test_curve_fit(data[:,(0,2)], fitted_curve, max_mean_err = true_cpr * 0.02, inlier_range = true_cpr * 0.02, max_outliers = len(data[:,0]) * 0.02) + + # encoder.phase + slope, offset, fitted_curve = fit_sawtooth(data[:,(0,3)], pi if reverse else -pi, -pi if reverse else pi, sigma=5) + test_assert_eq(slope / 7, 2*pi*true_rps, accuracy=0.05) + test_curve_fit(data[:,(0,3)], fitted_curve, max_mean_err = true_cpr * 0.02, inlier_range = true_cpr * 0.02, max_outliers = len(data[:,0]) * 0.02) + + # encoder.pos_estimate + slope, offset, fitted_curve = fit_line(data[:,(0,4)]) + test_assert_eq(slope, true_cps, accuracy=0.005) + test_curve_fit(data[:,(0,4)], fitted_curve, max_mean_err = true_cpr * 0.02, inlier_range = true_cpr * 0.02, max_outliers = len(data[:,0]) * 0.02) + + # encoder.pos_cpr + slope, offset, fitted_curve = fit_sawtooth(data[:,(0,5)], true_cpr if reverse else 0, 0 if reverse else true_cpr) + test_assert_eq(slope, true_cps, accuracy=0.005) + test_curve_fit(data[:,(0,5)], fitted_curve, max_mean_err = true_cpr * 0.05, inlier_range = true_cpr * 0.05, max_outliers = len(data[:,0]) * 0.02) + + # encoder.vel_estimate + slope, offset, fitted_curve = fit_line(data[:,(0,6)]) + test_assert_eq(slope, 0.0, range = true_cpr * abs(true_rps) * 0.01) + test_assert_eq(offset, true_cpr * true_rps, accuracy = 0.02) + test_curve_fit(data[:,(0,6)], fitted_curve, max_mean_err = true_cpr * 0.05, inlier_range = true_cpr * 0.05 * noise, max_outliers = len(data[:,0]) * 0.05) + + + +teensy_incremental_encoder_emulation_code = """ +void setup() { + pinMode({enc_a}, OUTPUT); + pinMode({enc_b}, OUTPUT); +} + +int cpr = 8192; +int rpm = 30; + +// the loop routine runs over and over again forever: +void loop() { + int microseconds_per_count = (1000000 * 60 / cpr / rpm); + + for (;;) { + digitalWrite({enc_a}, HIGH); + delayMicroseconds(microseconds_per_count); + digitalWrite({enc_b}, HIGH); + delayMicroseconds(microseconds_per_count); + digitalWrite({enc_a}, LOW); + delayMicroseconds(microseconds_per_count); + digitalWrite({enc_b}, LOW); + delayMicroseconds(microseconds_per_count); + } +} +""" + +class TestIncrementalEncoder(TestEncoderBase): + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for encoder in odrive.encoders: + # Find the Teensy that is connected to the encoder pins and the corresponding Teensy GPIOs + + gpio_conns = [ + testrig.get_directly_connected_components(encoder.a), + testrig.get_directly_connected_components(encoder.b), + ] + + valid_combinations = [ + (combination[0].parent,) + tuple(combination) + for combination in itertools.product(*gpio_conns) + if ((len(set(c.parent for c in combination)) == 1) and isinstance(combination[0].parent, TeensyComponent)) + ] + + yield (encoder, valid_combinations) + + + def run_test(self, enc: ODriveEncoderComponent, teensy: TeensyComponent, teensy_gpio_a: TeensyGpio, teensy_gpio_b: TeensyGpio, logger: Logger): + true_cps = 8192*0.5 # counts per second generated by the virtual encoder + + code = teensy_incremental_encoder_emulation_code.replace("{enc_a}", str(teensy_gpio_a.num)).replace("{enc_b}", str(teensy_gpio_b.num)) + teensy.compile_and_program(code) + + if enc.handle.config.mode != ENCODER_MODE_INCREMENTAL: + enc.handle.config.mode = ENCODER_MODE_INCREMENTAL + enc.parent.save_config_and_reboot() + else: + time.sleep(1.0) # wait for PLLs to stabilize + + enc.handle.config.bandwidth = 1000 + + logger.debug("testing with 8192 CPR...") + self.run_generic_encoder_test(enc.handle, 8192, true_cps / 8192) + logger.debug("testing with 65536 CPR...") + self.run_generic_encoder_test(enc.handle, 65536, true_cps / 65536) + enc.handle.config.cpr = 8192 + + + +teensy_sin_cos_encoder_emulation_code = """ +void setup() { + analogWriteResolution(10); + int freq = 150000000/1024; // ~146.5kHz PWM frequency + analogWriteFrequency({enc_sin}, freq); + analogWriteFrequency({enc_cos}, freq); +} + +float rps = 1.0f; +float pos = 0; + +void loop() { + pos += 0.001f * rps; + if (pos > 1.0f) + pos -= 1.0f; + analogWrite({enc_sin}, (int)(512.0f + 512.0f * sin(2.0f * M_PI * pos))); + analogWrite({enc_cos}, (int)(512.0f + 512.0f * cos(2.0f * M_PI * pos))); + delay(1); +} +""" + +class TestSinCosEncoder(TestEncoderBase): + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + gpio_conns = [ + testrig.get_directly_connected_components(odrive.gpio3), + testrig.get_directly_connected_components(odrive.gpio4), + ] + + valid_combinations = [ + (combination[0].parent,) + tuple(combination) + for combination in itertools.product(*gpio_conns) + if ((len(set(c.parent for c in combination)) == 1) and isinstance(combination[0].parent, TeensyComponent)) + ] + + yield (odrive.encoders[0], valid_combinations) + + + def run_test(self, enc: ODriveEncoderComponent, teensy: TeensyComponent, teensy_gpio_sin: TeensyGpio, teensy_gpio_cos: TeensyGpio, logger: Logger): + code = teensy_sin_cos_encoder_emulation_code.replace("{enc_sin}", str(teensy_gpio_sin.num)).replace("{enc_cos}", str(teensy_gpio_cos.num)) + teensy.compile_and_program(code) + + if enc.handle.config.mode != ENCODER_MODE_SINCOS: + enc.parent.unuse_gpios() + enc.handle.config.mode = ENCODER_MODE_SINCOS + enc.parent.save_config_and_reboot() + else: + time.sleep(1.0) # wait for PLLs to stabilize + + enc.handle.config.bandwidth = 100 + + self.run_generic_encoder_test(enc.handle, 6283, 1.0, 2.0) + + + +teensy_hall_effect_encoder_emulation_code = """ +void setup() { + pinMode({hall_a}, OUTPUT); + pinMode({hall_b}, OUTPUT); + pinMode({hall_c}, OUTPUT); + digitalWrite({hall_a}, HIGH); +} + +int cpr = 90; // 15 pole-pairs. Value suggested in hoverboard.md +float rps = 1.0f; +int us_per_count = (1000000.0f / cpr / rps); + +void loop() { + digitalWrite({hall_b}, HIGH); + delayMicroseconds(us_per_count); + digitalWrite({hall_a}, LOW); + delayMicroseconds(us_per_count); + digitalWrite({hall_c}, HIGH); + delayMicroseconds(us_per_count); + digitalWrite({hall_b}, LOW); + delayMicroseconds(us_per_count); + digitalWrite({hall_a}, HIGH); + delayMicroseconds(us_per_count); + digitalWrite({hall_c}, LOW); + delayMicroseconds(us_per_count); +} +""" + +class TestHallEffectEncoder(TestEncoderBase): + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for encoder in odrive.encoders: + # Find the Teensy that is connected to the encoder pins and the corresponding Teensy GPIOs + + gpio_conns = [ + testrig.get_directly_connected_components(encoder.a), + testrig.get_directly_connected_components(encoder.b), + testrig.get_directly_connected_components(encoder.z), + ] + + valid_combinations = [ + (combination[0].parent,) + tuple(combination) + for combination in itertools.product(*gpio_conns) + if ((len(set(c.parent for c in combination)) == 1) and isinstance(combination[0].parent, TeensyComponent)) + ] + + yield (encoder, valid_combinations) + + + def run_test(self, enc: ODriveEncoderComponent, teensy: TeensyComponent, teensy_gpio_a: TeensyGpio, teensy_gpio_b: TeensyGpio, teensy_gpio_c: TeensyGpio, logger: Logger): + true_cpr = 90 + true_rps = 1.0 + + code = teensy_hall_effect_encoder_emulation_code.replace("{hall_a}", str(teensy_gpio_a.num)).replace("{hall_b}", str(teensy_gpio_b.num)).replace("{hall_c}", str(teensy_gpio_c.num)) + teensy.compile_and_program(code) + + if enc.handle.config.mode != ENCODER_MODE_HALL: + enc.handle.config.mode = ENCODER_MODE_HALL + enc.parent.save_config_and_reboot() + else: + time.sleep(1.0) # wait for PLLs to stabilize + + enc.handle.config.bandwidth = 100 + + self.run_generic_encoder_test(enc.handle, true_cpr, true_rps) + enc.handle.config.cpr = 8192 + + + +# This encoder emulation mimics the specification given in the following datasheets: +# +# With {mode} == ENCODER_MODE_SPI_ABS_CUI: +# AMT23xx: https://www.cuidevices.com/product/resource/amt23.pdf +# +# With {mode} == ENCODER_MODE_SPI_ABS_AMS: +# AS5047P: https://ams.com/documents/20143/36005/AS5047P_DS000324_2-00.pdf/a7d44138-51f1-2f6e-c8b6-2577b369ace8 +# AS5048A/AS5048B: https://ams.com/documents/20143/36005/AS5048_DS000298_4-00.pdf/910aef1f-6cd3-cbda-9d09-41f152104832 +# => Only the read command on address 0x3fff is currently implemented. + +teensy_spi_encoder_emulation_code = """ +#define ENCODER_MODE_SPI_ABS_CUI 0x100 +#define ENCODER_MODE_SPI_ABS_AMS 0x101 +#define ENCODER_MODE_SPI_ABS_AEAT 0x102 + +static float rps = 1.0f; +static uint32_t cpr = 16384; +static uint32_t us_per_revolution = (uint32_t)(1000000.0f / rps); +static uint16_t spi_txd = 0; // first output word: NOP +static uint32_t zerotime = 0; + +void setup() { + pinMode({ncs}, INPUT_PULLUP); +} + +uint16_t get_pos_now() { + uint32_t time = micros(); + return ((uint64_t)((time - zerotime) % us_per_revolution)) * cpr / us_per_revolution; +} + + +#if {mode} == ENCODER_MODE_SPI_ABS_AMS + +uint8_t ams_parity(uint16_t v) { + v ^= v >> 8; + v ^= v >> 4; + v ^= v >> 2; + v ^= v >> 1; + return v & 1; +} + +uint16_t handle_command(uint16_t cmd) { + const uint16_t ERROR_RESPONSE = 0xc000; // error flag and parity bit set + + if (ams_parity(cmd)) { + return ERROR_RESPONSE; + } + + if (!(cmd & 14)) { // write not supported + return ERROR_RESPONSE; + } + + uint16_t addr = cmd & 0x3fff; + uint16_t data; + + switch (addr) { + case 0x3fff: data = get_pos_now(); break; + default: return ERROR_RESPONSE; + } + + return data | (ams_parity(data) << 15); +} + +#endif + +#if {mode} == ENCODER_MODE_SPI_ABS_CUI + +uint8_t cui_parity(uint16_t v) { + v ^= v >> 8; + v ^= v >> 4; + v ^= v >> 2; + return ~v & 3; +} + +uint16_t handle_command(uint16_t cmd) { + (void) cmd; // input not used on CUI + + // Test the cui_parity function itself with the example given in the datasheet + if ((0x21AB | (cui_parity(0x21AB) << 14)) != 0x61AB) { + return 0x0000; + } + + uint16_t data = get_pos_now(); + return data | (cui_parity(data) << 14); +} + +#endif + + +void loop() { + while (digitalReadFast({reset})) { + zerotime = micros(); + } + + if (!digitalReadFast({ncs})) { + static uint16_t spi_rxd = 0; + + pinMode({miso}, OUTPUT); + + for (;;) { + while (!digitalReadFast({sck})) + if (digitalReadFast({ncs})) + goto cs_deasserted; + + // Rising edge: Push output bit + + bool output_bit = spi_txd & 0x8000; + digitalWriteFast({miso}, output_bit); + spi_txd <<= 1; + + while (digitalReadFast({sck})) + if (digitalReadFast({ncs})) + goto cs_deasserted; + + // Falling edge: Sample input bit (only in AMS mode) + +#if {mode} == ENCODER_MODE_SPI_ABS_AMS + bool input_bit = digitalReadFast({mosi}); + spi_rxd <<= 1; + if (input_bit) { + spi_rxd |= 1; + } else { + spi_rxd &= ~1; + } +#endif + } + +cs_deasserted: + // chip deselected: Process command + pinMode({miso}, INPUT); + + spi_txd = handle_command(spi_rxd); + } +} +""" + +class TestSpiEncoder(TestEncoderBase): + def __init__(self, mode: int): + self.mode = mode + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for encoder in odrive.encoders: + odrive_ncs_gpio = odrive.gpio7 # this GPIO choice is completely arbitrary + gpio_conns = [ + testrig.get_connected_components(odrive.sck, TeensyGpio), + testrig.get_connected_components(odrive.miso, TeensyGpio), + testrig.get_connected_components(odrive.mosi, TeensyGpio), + testrig.get_connected_components(odrive_ncs_gpio, TeensyGpio), + ] + + valid_combinations = [] + for combination in itertools.product(*gpio_conns): + if (len(set(c.parent for c in combination)) != 1): + continue + teensy = combination[0].parent + reset_pin_options = [] + for gpio in teensy.gpios: + for local_gpio in testrig.get_connected_components(gpio, LinuxGpioComponent): + reset_pin_options.append((gpio, local_gpio)) + valid_combinations.append((teensy, *combination, reset_pin_options)) + + yield (encoder, 7, valid_combinations) + + + def run_test(self, enc: ODriveEncoderComponent, odrive_ncs_gpio: int, teensy: TeensyComponent, teensy_gpio_sck: TeensyGpio, teensy_gpio_miso: TeensyGpio, teensy_gpio_mosi: TeensyGpio, teensy_gpio_ncs: TeensyGpio, teensy_gpio_reset: TeensyGpio, reset_gpio: LinuxGpioComponent, logger: Logger): + true_cpr = 16384 + true_rps = 1.0 + + reset_gpio.config(output=True) # hold encoder and disable its SPI + reset_gpio.write(True) + + code = (teensy_spi_encoder_emulation_code + .replace("{sck}", str(teensy_gpio_sck.num)) + .replace("{miso}", str(teensy_gpio_miso.num)) + .replace("{mosi}", str(teensy_gpio_mosi.num)) + .replace("{ncs}", str(teensy_gpio_ncs.num)) + .replace("{reset}", str(teensy_gpio_reset.num)) + .replace("{mode}", str(self.mode))) + teensy.compile_and_program(code) + + logger.debug(f'Configuring absolute encoder in mode 0x{self.mode:x}...') + enc.handle.config.mode = self.mode + enc.handle.config.abs_spi_cs_gpio_pin = odrive_ncs_gpio + enc.handle.config.cpr = true_cpr + # Also put the other encoder into SPI mode to make it more interesting + other_enc = enc.parent.encoders[1 - enc.num] + other_enc.handle.config.mode = self.mode + other_enc.handle.config.abs_spi_cs_gpio_pin = odrive_ncs_gpio + other_enc.handle.config.cpr = true_cpr + enc.parent.save_config_and_reboot() + + time.sleep(1.0) + + logger.debug('Testing absolute readings and SPI errors...') + + # Encoder is still disabled - expect recurring error + enc.handle.error = 0 + time.sleep(0.002) + # This fails from time to time because the pull-up on the ODrive only manages + # to pull MISO to 1.8V, leaving it in the undefined range. + test_assert_eq(enc.handle.error, ENCODER_ERROR_ABS_SPI_COM_FAIL) + + # Enable encoder and expect error to go away + reset_gpio.write(False) + release_time = time.monotonic() + enc.handle.error = 0 + time.sleep(0.002) + test_assert_eq(enc.handle.error, 0) + + # Check absolute position after 1.5s + time.sleep(1.5) + true_delta_t = time.monotonic() - release_time + test_assert_eq(enc.handle.pos_abs, (true_delta_t * true_rps * true_cpr) % true_cpr, range = true_cpr*0.001) + + test_assert_eq(enc.handle.error, 0) + reset_gpio.write(True) + time.sleep(0.002) + test_assert_eq(enc.handle.error, ENCODER_ERROR_ABS_SPI_COM_FAIL) + reset_gpio.write(False) + release_time = time.monotonic() + enc.handle.error = 0 + time.sleep(0.002) + test_assert_eq(enc.handle.error, 0) + + # Check absolute position after 1.5s + time.sleep(1.5) + true_delta_t = time.monotonic() - release_time + test_assert_eq(enc.handle.pos_abs, (true_delta_t * true_rps * true_cpr) % true_cpr, range = true_cpr*0.001) + + self.run_generic_encoder_test(enc.handle, true_cpr, true_rps) + enc.handle.config.cpr = 8192 + + + +if __name__ == '__main__': + test_runner.run([ + TestIncrementalEncoder(), + TestSinCosEncoder(), + TestHallEffectEncoder(), + TestSpiEncoder(ENCODER_MODE_SPI_ABS_AMS), + TestSpiEncoder(ENCODER_MODE_SPI_ABS_CUI), + ]) diff --git a/tools/odrive/tests/endstop_test.py b/tools/odrive/tests/endstop_test.py new file mode 100644 index 00000000..fd07ae8d --- /dev/null +++ b/tools/odrive/tests/endstop_test.py @@ -0,0 +1,34 @@ +import odrive +from odrive.enums import * +from odrive.utils import * + +print("finding an odrive...") +odrv0 = odrive.find_any() +print('Odrive found') + +odrv0.axis1.controller.config.vel_limit = 50000 +odrv0.axis1.controller.config.control_mode = CONTROL_MODE_POSITION_CONTROL +odrv0.axis1.controller.config.input_mode = INPUT_MODE_PASSTHROUGH +odrv0.axis1.encoder.config.cpr = 2400 +odrv0.axis1.encoder.config.bandwidth = 1000 +odrv0.axis1.motor.config.calibration_current = 5 +odrv0.axis1.motor.config.current_lim = 5 +odrv0.axis1.controller.config.homing_speed = 5000 +odrv0.config.brake_resistance = 0 + +odrv0.axis0.min_endstop.config.gpio_num = 6 +odrv0.axis0.min_endstop.config.enabled = True +odrv0.axis0.min_endstop.config.offset = -1000 +odrv0.axis0.max_endstop.config.gpio_num = 5 +odrv0.axis0.max_endstop.config.enabled = True + +odrv0.axis1.min_endstop.config.gpio_num = 8 +odrv0.axis1.min_endstop.config.enabled = True +odrv0.axis1.min_endstop.config.offset = -1000 +odrv0.axis1.max_endstop.config.gpio_num = 7 +odrv0.axis1.max_endstop.config.enabled = True + +odrv0.axis1.config.startup_encoder_offset_calibration = True +odrv0.axis1.config.startup_motor_calibration = True +odrv0.axis1.config.startup_homing = True +odrv0.axis1.config.startup_closed_loop_control = True diff --git a/tools/odrive/tests/fibre_test.py b/tools/odrive/tests/fibre_test.py new file mode 100644 index 00000000..75801a5b --- /dev/null +++ b/tools/odrive/tests/fibre_test.py @@ -0,0 +1,56 @@ + +import test_runner + +import time + +from fibre.utils import Logger +from odrive.enums import * +from test_runner import * + +class FibreFunctionalTest(): + """ + Tests basic protocol functionality. + """ + + def get_test_cases(self, testrig: TestRig): + return testrig.get_components(ODriveComponent) + + def run_test(self, odrive: ODriveComponent, logger: Logger): + # Test property read/write + odrive.handle.test_property = 42 + test_assert_eq(odrive.handle.test_property, 42) + odrive.handle.test_property = 0xffffffff + test_assert_eq(odrive.handle.test_property, 0xffffffff) + + # Test function call + val = odrive.handle.get_adc_voltage(0) + test_assert_within(val, 0.01, 3.29) + + # Test custom setter (aka property write hook) + odrive.handle.axis0.motor.config.phase_resistance = 1 + odrive.handle.axis0.motor.config.phase_inductance = 1 + odrive.handle.axis0.motor.config.current_control_bandwidth = 1000 + old_gain = odrive.handle.axis0.motor.current_control.p_gain + test_assert_eq(old_gain, 1000, accuracy=0.0001) # must be non-zero for subsequent check to work + odrive.handle.axis0.motor.config.current_control_bandwidth /= 2 + test_assert_eq(odrive.handle.axis0.motor.current_control.p_gain, old_gain / 2, accuracy=0.0001) + +class FibreBurnInTest(): + """ + Tests continuous usage of the protocol. + """ + + def get_test_cases(self, testrig: TestRig): + return testrig.get_components(ODriveComponent) + + def run_test(self, odrive: ODriveComponent, logger: Logger): + data = record_log(lambda: [odrive.handle.vbus_voltage], duration=10.0) + expected_data = np.mean(data[:,1]) * np.ones(data[:,1].size) + test_curve_fit(data, expected_data, max_mean_err = 0.1, inlier_range = 0.5, max_outliers = 0) + + +if __name__ == '__main__': + test_runner.run([ + FibreFunctionalTest(), + FibreBurnInTest(), + ]) diff --git a/tools/odrive/tests/integration_test.py b/tools/odrive/tests/integration_test.py new file mode 100644 index 00000000..0f66d8c6 --- /dev/null +++ b/tools/odrive/tests/integration_test.py @@ -0,0 +1,230 @@ +# this test runs the motor using CAN +# TODO - run a motor using all common use cases (uart, step/dir, pwm) + +import test_runner + +import struct +import can +import asyncio +import time +import math + +from fibre.utils import Logger +from odrive.enums import * +from test_runner import * + +# Each argument is described as tuple (name, format, scale). +# Struct format codes: https://docs.python.org/2/library/struct.html +command_set = { + 'heartbeat': (0x001, [('error', 'I', 1), ('current_state', 'I', 1)]), # tested + 'estop': (0x002, []), # tested + 'get_motor_error': (0x003, [('motor_error', 'I', 1)]), # untested + 'get_encoder_error': (0x004, [('encoder_error', 'I', 1)]), # untested + 'get_sensorless_error': (0x005, [('sensorless_error', 'I', 1)]), # untested + 'set_node_id': (0x006, [('node_id', 'I', 1)]), # tested + 'set_requested_state': (0x007, [('requested_state', 'I', 1)]), # tested + # 0x008 not yet implemented + 'get_encoder_estimates': (0x009, [('encoder_pos_estimate', 'f', 1), ('encoder_vel_estimate', 'f', 1)]), # partially tested + 'get_encoder_count': (0x00a, [('encoder_shadow_count', 'i', 1), ('encoder_count', 'i', 1)]), # partially tested + 'set_controller_modes': (0x00b, [('control_mode', 'i', 1), ('input_mode', 'i', 1)]), # tested + 'set_input_pos': (0x00c, [('input_pos', 'f', 1), ('vel_ff', 'h', 0.001), ('torque_ff', 'h', 0.001)]), # tested + 'set_input_vel': (0x00d, [('input_vel', 'f', 1), ('torque_ff', 'f', 1)]), # tested + 'set_input_torque': (0x00e, [('input_torque', 'f', 1)]), # tested + 'set_velocity_limit': (0x00f, [('velocity_limit', 'f', 1)]), # tested + 'start_anticogging': (0x010, []), # untested + 'set_traj_vel_limit': (0x011, [('traj_vel_limit', 'f', 1)]), # tested + 'set_traj_accel_limits': (0x012, [('traj_accel_limit', 'f', 1), ('traj_decel_limit', 'f', 1)]), # tested + 'set_traj_inertia': (0x013, [('inertia', 'f', 1)]), # tested + 'get_iq': (0x014, [('iq_setpoint', 'f', 1), ('iq_measured', 'f', 1)]), # untested + 'get_sensorless_estimates': (0x015, [('sensorless_pos_estimate', 'f', 1), ('sensorless_vel_estimate', 'f', 1)]), # untested + 'reboot': (0x016, []), # tested + 'get_vbus_voltage': (0x017, [('vbus_voltage', 'f', 1)]), # tested + 'clear_errors': (0x018, []), # partially tested +} + +def command(bus, node_id_, extended_id, cmd_name, **kwargs): + cmd_spec = command_set[cmd_name] + cmd_id = cmd_spec[0] + fmt = '<' + ''.join([f for (n, f, s) in cmd_spec[1]]) # all little endian + + if (sorted([n for (n, f, s) in cmd_spec[1]]) != sorted(kwargs.keys())): + raise Exception("expected arguments: " + str([n for (n, f, s) in cmd_spec[1]])) + + fields = [((kwargs[n] / s) if f == 'f' else int(kwargs[n] / s)) for (n, f, s) in cmd_spec[1]] + data = struct.pack(fmt, *fields) + msg = can.Message(arbitration_id=((node_id_ << 5) | cmd_id), extended_id=extended_id, data=data) + bus.send(msg) + +async def record_messages(bus, node_id, extended_id, cmd_name, timeout = 5.0): + """ + Returns an async generator that yields a dictionary for each CAN message that + is received, provided that the CAN ID matches the expected value. + """ + + cmd_spec = command_set[cmd_name] + cmd_id = cmd_spec[0] + fmt = '<' + ''.join([f for (n, f, s) in cmd_spec[1]]) # all little endian + + reader = can.AsyncBufferedReader() + notifier = can.Notifier(bus, [reader], timeout = timeout, loop = asyncio.get_event_loop()) + + try: + # The timeout in can.Notifier only triggers if no new messages are received at all, + # so we need a second monitoring method. + start = time.monotonic() + while True: + msg = await reader.get_message() + if ((msg.arbitration_id == ((node_id << 5) | cmd_id)) and (msg.is_extended_id == extended_id) and not msg.is_remote_frame): + fields = struct.unpack(fmt, msg.data[:(struct.calcsize(fmt))]) + res = {n: (fields[i] * s) for (i, (n, f, s)) in enumerate(cmd_spec[1])} + res['t'] = time.monotonic() + yield res + if (time.monotonic() - start) > timeout: + break + finally: + notifier.stop() + +async def request(bus, node_id, extended_id, cmd_name, timeout = 1.0): + cmd_spec = command_set[cmd_name] + cmd_id = cmd_spec[0] + + msg_generator = record_messages(bus, node_id, extended_id, cmd_name, timeout) + + msg = can.Message(arbitration_id=((node_id << 5) | cmd_id), extended_id=extended_id, data=[], is_remote_frame=True) + bus.send(msg) + + async for msg in msg_generator: + return msg + + raise TimeoutError() + +async def get_all(async_iterator): + return [x async for x in async_iterator] + +class TestSimpleCANClosedLoop(): + def prepare(self, odrive: ODriveComponent, canbus: CanInterfaceComponent, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, node_id: int, extended_id: bool, logger: Logger): + # Make sure there are no funny configurations active + logger.debug('Setting up clean configuration...') + axis_ctx.parent.erase_config_and_reboot() + + # run calibration + axis_ctx.handle.requested_state = AXIS_STATE_FULL_CALIBRATION_SEQUENCE + while axis_ctx.handle.current_state != AXIS_STATE_IDLE: + time.sleep(1) + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_no_error(axis_ctx) + + # Return a context that can be used in a with-statement. + class safe_terminator(): + def __enter__(self): + pass + def __exit__(self, exc_type, exc_val, exc_tb): + logger.debug('clearing config...') + axis_ctx.handle.requested_state = AXIS_STATE_IDLE + time.sleep(0.005) + axis_ctx.parent.erase_config_and_reboot() + return safe_terminator() + + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + can_interfaces = list(testrig.get_connected_components(odrive.can, CanInterfaceComponent)) + for num in range(2): + encoders = testrig.get_connected_components({ + 'a': (odrive.encoders[num].a, False), + 'b': (odrive.encoders[num].b, False) + }, EncoderComponent) + motors = testrig.get_connected_components(odrive.axes[num], MotorComponent) + for motor, encoder in itertools.product(motors, encoders): + if encoder.impl in testrig.get_connected_components(motor): + yield (odrive, can_interfaces, odrive.axes[num], motor, encoder, 0, False) + + def run_test(self, odrive: ODriveComponent, canbus: CanInterfaceComponent, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, node_id: int, extended_id: bool, logger: Logger): + # this test is a sanity check to make sure that closed loop operation works + # actual testing of closed loop functionality should be tested using closed_loop_test.py + + with self.prepare(odrive, canbus, axis_ctx, motor_ctx, enc_ctx, node_id, extended_id, logger): + def my_cmd(cmd_name, **kwargs): command(canbus.handle, node_id, extended_id, cmd_name, **kwargs) + def my_req(cmd_name, **kwargs): return asyncio.run(request(canbus.handle, node_id, extended_id, cmd_name, **kwargs)) + def fence(): my_req('get_vbus_voltage') # fence to ensure the CAN command was sent + + # make sure no gpio input is overwriting our values + odrive.unuse_gpios() + + axis_ctx.handle.config.enable_watchdog = False + axis_ctx.handle.clear_errors() + axis_ctx.handle.config.can_node_id = node_id + axis_ctx.handle.config.can_node_id_extended = extended_id + time.sleep(0.1) + + my_cmd('set_node_id', node_id=node_id+20) + asyncio.run(request(canbus.handle, node_id+20, extended_id, 'get_vbus_voltage')) + test_assert_eq(axis_ctx.handle.config.can_node_id, node_id+20) + + # Reset node ID to default value + command(canbus.handle, node_id+20, extended_id, 'set_node_id', node_id=node_id) + fence() + test_assert_eq(axis_ctx.handle.config.can_node_id, node_id) + + vel_limit = 15.0 + nominal_vel = 10.0 + axis_ctx.handle.controller.config.vel_limit = vel_limit + axis_ctx.handle.motor.config.current_lim = 30.0 + + my_cmd('set_requested_state', requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL) + fence() + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_CLOSED_LOOP_CONTROL) + test_assert_no_error(axis_ctx) + + start_pos = axis_ctx.handle.encoder.pos_estimate + + # position test + logger.debug('Position control test') + my_cmd('set_controller_modes', control_mode=CONTROL_MODE_POSITION_CONTROL, input_mode=INPUT_MODE_PASSTHROUGH) # position control, passthrough + fence() + my_cmd('set_input_pos', input_pos=1.0, vel_ff=0, torque_ff=0) + fence() + test_assert_eq(axis_ctx.handle.controller.input_pos, 1.0, range=0.1) + time.sleep(2) + test_assert_eq(axis_ctx.handle.encoder.pos_estimate, start_pos + 1.0, range=0.1) + my_cmd('set_input_pos', input_pos=0, vel_ff=0, torque_ff=0) + fence() + time.sleep(2) + + test_assert_no_error(axis_ctx) + + # velocity test + logger.debug('Velocity control test') + my_cmd('set_controller_modes', control_mode=CONTROL_MODE_VELOCITY_CONTROL, input_mode=INPUT_MODE_PASSTHROUGH) # velocity control, passthrough + fence() + my_cmd('set_input_vel', input_vel = nominal_vel, torque_ff=0) + fence() + time.sleep(5) + test_assert_eq(axis_ctx.handle.encoder.vel_estimate, nominal_vel, range=nominal_vel * 0.05) # big range here due to cogging and other issues + my_cmd('set_input_vel', input_vel = 0, torque_ff=0) + fence() + time.sleep(2) + + test_assert_no_error(axis_ctx) + + # torque test + logger.debug('Torque control test') + my_cmd('set_controller_modes', control_mode=CONTROL_MODE_TORQUE_CONTROL, input_mode=INPUT_MODE_PASSTHROUGH) # torque control, passthrough + fence() + my_cmd('set_input_torque', input_torque=0.5) + fence() + time.sleep(5) + test_assert_eq(axis_ctx.handle.controller.input_torque, 0.5, range=0.1) + my_cmd('set_input_torque', input_torque = 0) + fence() + time.sleep(2) + + test_assert_no_error(axis_ctx) + + # go back to idle + my_cmd('set_requested_state', requested_state = AXIS_STATE_IDLE) + fence() + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + +if __name__ == '__main__': + test_runner.run(TestSimpleCANClosedLoop()) \ No newline at end of file diff --git a/tools/odrive/tests/not_a_test.py b/tools/odrive/tests/not_a_test.py new file mode 100644 index 00000000..7be3e995 --- /dev/null +++ b/tools/odrive/tests/not_a_test.py @@ -0,0 +1,30 @@ + +import test_runner + +from fibre.utils import Logger +from test_runner import * + +class EncoderPassthrough(): + """ + Does nothing except passing encoder0 through. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + for num in range(1): + encoders = testrig.get_connected_components({ + 'a': (odrive.encoders[num].a, False), + 'b': (odrive.encoders[num].b, False), + 'z': (odrive.encoders[num].z, False) + }, EncoderComponent) + motors = testrig.get_connected_components(odrive.axes[num], MotorComponent) + + for motor, encoder in itertools.product(motors, encoders): + if encoder.impl in testrig.get_connected_components(motor): + yield (odrive.axes[num], motor, encoder) + + def run_test(self, axis_ctx: ODriveAxisComponent, motor_ctx: MotorComponent, enc_ctx: EncoderComponent, logger: Logger): + logger.debug(f'Encoder {axis_ctx.num} was passed through') + +if __name__ == '__main__': + test_runner.run(EncoderPassthrough()) diff --git a/tools/odrive/tests/nvm_test.py b/tools/odrive/tests/nvm_test.py new file mode 100644 index 00000000..72ed2061 --- /dev/null +++ b/tools/odrive/tests/nvm_test.py @@ -0,0 +1,46 @@ + +import test_runner + +import time +from math import pi +import os + +import fibre +from fibre.utils import Logger +from test_runner import * + +class TestStoreAndReboot(): + """ + Stores the current configuration to NVM and reboots. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + yield (odrive,) + + def run_with_values(self, odrive: ODriveComponent, values: list, logger: Logger): + logger.debug("storing configuration and rebooting...") + + for value in values: + odrive.handle.config.brake_resistance = value + + odrive.handle.save_configuration() + try: + odrive.handle.reboot() + except fibre.ChannelBrokenException: + pass # this is expected + odrive.handle = None + time.sleep(2) + + odrive.prepare(logger) + + logger.debug("verifying configuration after reboot...") + test_assert_eq(odrive.handle.config.brake_resistance, values[-1], accuracy=0.01) + + def run_test(self, odrive: ODriveComponent, logger: Logger): + self.run_with_values(odrive, [0.5, 1.0, 1.5], logger) + self.run_with_values(odrive, [2.5, 3.7], logger) + self.run_with_values(odrive, [0.47], logger) + +if __name__ == '__main__': + test_runner.run(TestStoreAndReboot()) diff --git a/tools/odrive/tests.py b/tools/odrive/tests/old_tests.py similarity index 92% rename from tools/odrive/tests.py rename to tools/odrive/tests/old_tests.py index 264fa62b..fac90f1d 100644 --- a/tools/odrive/tests.py +++ b/tools/odrive/tests/old_tests.py @@ -17,31 +17,10 @@ print = functools.partial(print, flush=True) import abc ABC = abc.ABC -class TestFailed(Exception): - def __init__(self, message): - Exception.__init__(self, message) class PreconditionsNotMet(Exception): pass -class ODriveTestContext(): - def __init__(self, name: str, yaml: dict): - self.handle = None - self.yaml = yaml - self.name = name - self.axes = [] - for axis_idx, axis_yaml in enumerate(yaml['axes']): - axis_name = (name + "." + axis_yaml['name']) if 'name' in axis_yaml else '{}.axis{}'.format(name, axis_idx) - self.axes.append(AxisTestContext(axis_name, axis_yaml, self)) - - def rediscover(self): - """ - Reconnects to the ODrive - """ - self.handle = odrive.find_any( - path="usb", serial_number=self.yaml['serial-number'], timeout=15)#, printer=print) - for axis_idx, axis_ctx in enumerate(self.axes): - axis_ctx.handle = self.handle.__dict__['axis{}'.format(axis_idx)] class AxisTestContext(): def __init__(self, name: str, yaml: dict, odrv_ctx: ODriveTestContext): @@ -51,24 +30,6 @@ class AxisTestContext(): self.lock = threading.Lock() self.odrv_ctx = odrv_ctx -def test_assert_eq(observed, expected, range=None, accuracy=None): - sign = lambda x: 1 if x >= 0 else -1 - - # Comparision with absolute range - if not range is None: - if (observed < expected - range) or (observed > expected + range): - raise TestFailed("value out of range: expected {}+-{} but observed {}".format(expected, range, observed)) - - # Comparision with relative range - elif not accuracy is None: - if sign(observed) != sign(expected) or (abs(observed) < abs(expected) * (1 - accuracy)) or (abs(observed) > abs(expected) * (1 + accuracy)): - raise TestFailed("value out of range: expected {}+-{}% but observed {}".format(expected, accuracy*100.0, observed)) - - # Exact comparision - else: - if observed != expected: - raise TestFailed("value mismatch: expected {} but observed {}".format(expected, observed)) - def get_errors(axis_ctx: AxisTestContext): errors = [] if axis_ctx.handle.motor.error != 0: @@ -398,29 +359,6 @@ class TestClosedLoopControl(AxisTest): time.sleep(0.5) request_state(axis_ctx, AXIS_STATE_IDLE) -class TestStoreAndReboot(ODriveTest): - """ - Stores the current configuration to NVM and reboots. - """ - def run_test(self, odrv_ctx: ODriveTestContext, logger): - logger.debug("storing configuration and rebooting...") - odrv_ctx.handle.save_configuration() - try: - odrv_ctx.handle.reboot() - except fibre.ChannelBrokenException: - pass # this is expected - time.sleep(2) - - odrv_ctx.rediscover() - - logger.debug("verifying configuration after reboot...") - test_assert_eq(odrv_ctx.handle.config.brake_resistance, odrv_ctx.yaml['brake-resistance'], accuracy=0.01) - for axis_ctx in odrv_ctx.axes: - test_assert_eq(axis_ctx.handle.encoder.config.cpr, axis_ctx.yaml['encoder-cpr']) - test_assert_eq(axis_ctx.handle.motor.config.phase_resistance, axis_ctx.yaml['motor-phase-resistance'], accuracy=0.2) - test_assert_eq(axis_ctx.handle.motor.config.phase_inductance, axis_ctx.yaml['motor-phase-inductance'], accuracy=0.5) - - class TestHighVelocity(AxisTest): """ Spins the motor up to it's max speed during a period of 10s. @@ -643,7 +581,7 @@ class TestVelCtrlVsPosCtrl(DualAxisTest): # Set up viscous fluid load logger.debug("activating load on {}...".format(load_ctx.name)) load_ctx.handle.controller.config.vel_integrator_gain = 0 - load_ctx.handle.controller.vel_integrator_current = 0 + load_ctx.handle.controller.vel_integrator_torque = 0 set_limits(load_ctx, logger, vel_limit=100000, current_limit=50) load_ctx.handle.controller.set_vel_setpoint(0, 0) request_state(load_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) diff --git a/tools/odrive/tests/pwm_input_test.py b/tools/odrive/tests/pwm_input_test.py new file mode 100644 index 00000000..a1945997 --- /dev/null +++ b/tools/odrive/tests/pwm_input_test.py @@ -0,0 +1,89 @@ + +import test_runner + +import time +import math +import os + +from odrive.enums import * +from test_runner import * + + +teensy_code_template = """ +float position = 0; // between 0 and 1 +float velocity = 1; // [position per second] + +void setup() { + pinMode({pwm_gpio}, OUTPUT); +} + +// the loop routine runs over and over again forever: +void loop() { + int high_microseconds = 1000 + (int)(position * 1000.0f); + + digitalWrite({pwm_gpio}, HIGH); + delayMicroseconds(high_microseconds); + digitalWrite({pwm_gpio}, LOW); + + // Wait for a total of 20ms. + // delayMicroseconds() only works well for values <= 16383 + delayMicroseconds(10000 - high_microseconds); + delayMicroseconds(10000); + + position += velocity * 0.02; + while (position > 1.0) + position -= 1.0; +} +""" + + +class TestPwmInput(): + """ + Verifies the PWM input. + + The Teensy generates a PWM signal that goes from 0% (1ms high) to 100% (2ms high) + in 1 second and then resumes at 0%. + + Note: this test is currently only written for ODrive 3.6 (or similar GPIO layout). + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + # Run a separate test for each PWM-capable GPIO. Use different min/max settings for each test. + yield (odrive, 1, -50, 200, list(testrig.get_connected_components(odrive.gpio1, TeensyGpio))) + yield (odrive, 2, 20, 400, list(testrig.get_connected_components(odrive.gpio2, TeensyGpio))) + yield (odrive, 3, -1000, 0, list(testrig.get_connected_components(odrive.gpio3, TeensyGpio))) + yield (odrive, 4, -20000, 20000, list(testrig.get_connected_components(odrive.gpio4, TeensyGpio))) + + def run_test(self, odrive: ODriveComponent, odrive_gpio_num: int, min_val: float, max_val: float, teensy_gpio: Component, logger: Logger): + teensy = teensy_gpio.parent + code = teensy_code_template.replace("{pwm_gpio}", str(teensy_gpio.num)) + teensy.compile_and_program(code) + + logger.debug("Set up PWM input...") + odrive.unuse_gpios() + + pwm_mapping = [ + odrive.handle.config.gpio1_pwm_mapping, + odrive.handle.config.gpio2_pwm_mapping, + odrive.handle.config.gpio3_pwm_mapping, + odrive.handle.config.gpio4_pwm_mapping + ][odrive_gpio_num - 1] + + pwm_mapping.endpoint = odrive.handle.axis0.controller._remote_attributes['input_pos'] + pwm_mapping.min = min_val + pwm_mapping.max = max_val + + odrive.save_config_and_reboot() + + data = record_log(lambda: [odrive.handle.axis0.controller.input_pos], duration=5.0) + + full_scale = max_val - min_val + slope, offset, fitted_curve = fit_sawtooth(data, min_val, max_val) + test_assert_eq(slope, full_scale / 1.0, accuracy=0.001) + test_curve_fit(data, fitted_curve, max_mean_err = full_scale * 0.05, inlier_range = full_scale * 0.05, max_outliers = len(data[:,0]) * 0.01) + + + +if __name__ == '__main__': + test_runner.run(TestPwmInput()) diff --git a/tools/odrive/tests/step_dir_test.py b/tools/odrive/tests/step_dir_test.py new file mode 100644 index 00000000..948a5eca --- /dev/null +++ b/tools/odrive/tests/step_dir_test.py @@ -0,0 +1,92 @@ + +import test_runner + +import struct +import asyncio +import time + +from fibre.utils import Logger +from odrive.enums import * +from test_runner import * + +class TestStepDir(): + """ + Tests Step/Dir input. + Not all possible combinations are tested, but each axis and each GPIO + participates in at least one test case. + + The tests are conducted while the axis is in idle. + """ + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + gpio_conns = [ + list(testrig.get_connected_components((odrive.gpio1, False), LinuxGpioComponent)), + list(testrig.get_connected_components((odrive.gpio2, False), LinuxGpioComponent)), + #list(testrig.get_connected_components((odrive.gpio3, False), LinuxGpioComponent)), # connected to LPF on test rig + #list(testrig.get_connected_components((odrive.gpio4, False), LinuxGpioComponent)), # connected to LPF on test rig + list(testrig.get_connected_components((odrive.gpio5, False), LinuxGpioComponent)), + list(testrig.get_connected_components((odrive.gpio6, False), LinuxGpioComponent)), + list(testrig.get_connected_components((odrive.gpio7, False), LinuxGpioComponent)), + list(testrig.get_connected_components((odrive.gpio8, False), LinuxGpioComponent)), + ] + + yield (odrive.axes[0], 1, gpio_conns[0], 2, gpio_conns[1]) + yield (odrive.axes[0], 5, gpio_conns[2], 6, gpio_conns[3]) + yield (odrive.axes[0], 7, gpio_conns[4], 8, gpio_conns[5]) # broken + # yield (odrive.axes[0], 7, gpio_conns[6], 8, gpio_conns[7]) # broken + + yield (odrive.axes[1], 7, gpio_conns[4], 8, gpio_conns[5]) + + def run_test(self, axis: ODriveAxisComponent, step_gpio_num: int, step_gpio: LinuxGpioComponent, dir_gpio_num: int, dir_gpio: LinuxGpioComponent, logger: Logger): + step_gpio.config(output=True) + step_gpio.write(False) + dir_gpio.config(output=True) + dir_gpio.write(True) + + if axis.num == 0: + axis.parent.handle.config.enable_uart = False + axis.handle.config.enable_step_dir = True + axis.handle.config.step_dir_always_on = True # needed for testing + axis.handle.config.step_gpio_pin = step_gpio_num + axis.handle.config.dir_gpio_pin = dir_gpio_num + request_state(axis, AXIS_STATE_IDLE) # apply step_dir_always_on config + + + ref = axis.handle.controller.input_pos + axis.handle.config.turns_per_step = turns_per_step = 10 + + # On the RPi 4 a ~5kHz GPIO signal can be generated from Python + + for i in range(100): + step_gpio.write(True) + step_gpio.write(False) + test_assert_eq(axis.handle.controller.input_pos, ref + (i + 1) * turns_per_step, range = 0.4 * turns_per_step) + + ref = axis.handle.controller.input_pos + dir_gpio.write(False) + + for i in range(100): + step_gpio.write(True) + step_gpio.write(False) + test_assert_eq(axis.handle.controller.input_pos, ref - (i + 1) * turns_per_step, range = 0.4 * turns_per_step) + + ref = axis.handle.controller.input_pos + dir_gpio.write(True) + axis.handle.config.turns_per_step = turns_per_step = 1 + + for i in range(100): + step_gpio.write(True) + step_gpio.write(False) + test_assert_eq(axis.handle.controller.input_pos, ref + (i + 1) * turns_per_step, range = 0.4 * turns_per_step) + + ref = axis.handle.controller.input_pos + axis.handle.config.turns_per_step = turns_per_step = -1 + + for i in range(100): + step_gpio.write(True) + step_gpio.write(False) + test_assert_eq(axis.handle.controller.input_pos, ref + (i + 1) * turns_per_step, range = 0.4 * abs(turns_per_step)) + + +if __name__ == '__main__': + test_runner.run(TestStepDir()) diff --git a/tools/odrive/tests/test_runner.py b/tools/odrive/tests/test_runner.py new file mode 100644 index 00000000..9633b92f --- /dev/null +++ b/tools/odrive/tests/test_runner.py @@ -0,0 +1,811 @@ +# Provides utilities for standalone test scripts. +# This script is not intended to be run directly. + +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + +import stat +import odrive +from odrive.enums import * +import fibre +from fibre import Logger, Event +import argparse +import yaml +from inspect import signature +import itertools +import time +import tempfile +import io +from typing import Union, Tuple + +# needed for curve fitting +import numpy as np +import scipy.optimize +import scipy.ndimage.filters + + +# Assert utils ----------------------------------------------------------------# + +class TestFailed(Exception): + def __init__(self, message): + Exception.__init__(self, message) + +def test_assert_eq(observed, expected, range=None, accuracy=None): + sign = lambda x: 1 if x >= 0 else -1 + + # Comparision with absolute range + if not range is None: + if (observed < expected - range) or (observed > expected + range): + raise TestFailed("value out of range: expected {}+-{} but observed {}".format(expected, range, observed)) + + # Comparision with relative range + elif not accuracy is None: + if sign(observed) != sign(expected) or (abs(observed) < abs(expected) * (1 - accuracy)) or (abs(observed) > abs(expected) * (1 + accuracy)): + raise TestFailed("value out of range: expected {}+-{}% but observed {}".format(expected, accuracy*100.0, observed)) + + # Exact comparision + else: + if observed != expected: + raise TestFailed("value mismatch: expected {} but observed {}".format(expected, observed)) + +def test_assert_within(observed, lower_bound, upper_bound, accuracy=0.0): + """ + Checks if the value is within the closed interval [lower_bound, upper_bound] + The permissible range can be expanded in both direction by the coefficiont "accuracy". + I.e. accuracy of 1.0 would expand the range by a total factor of 3.0 + """ + + lower_bound, upper_bound = ( + (lower_bound - (upper_bound - lower_bound) * accuracy), + (upper_bound + (upper_bound - lower_bound) * accuracy) + ) + + if (observed < lower_bound) or (observed > upper_bound): + raise TestFailed(f"the oberved value {observed} is outside the interval [{lower_bound}, {upper_bound}]") + + +# Other utils -----------------------------------------------------------------# + +def disjoint_sets(list_of_sets: list): + while len(list_of_sets): + current_set, list_of_sets = list_of_sets[0], list_of_sets[1:] + did_update = True + while did_update: + did_update = False + for i, s in enumerate(list_of_sets): + if len(current_set.intersection(s)): + current_set = current_set.union(s) + list_of_sets = list_of_sets[:i] + list_of_sets[(i+1):] + did_update = True + yield current_set + +def is_list_like(arg): + return hasattr(arg, '__iter__') and not isinstance(arg, str) + +def all_unique(lst): + seen = list() + return not any(i in seen or seen.append(i) for i in lst) + +def modpm(val, range): + return ((val + (range / 2)) % range) - (range / 2) + +def clamp(val, lower_bound, upper_bound): + return min(max(val, lower_bound), upper_bound) + +def record_log(data_getter, duration=5.0): + logger.debug(f"Recording log for {duration}s...") + data = [] + start = time.monotonic() + while time.monotonic() - start < duration: + data.append((time.monotonic() - start,) + tuple(data_getter())) + return np.array(data) + +def save_log(data, id=None): + import json + filename = '/tmp/log{}.json'.format('' if id is None else str(id)) + with open(filename, 'w+') as fp: + json.dump(data.tolist(), fp, indent=2) + print(f'data saved to {filename}') + +def fit_line(data): + func = lambda x, a, b: x*a + b + slope, offset = scipy.optimize.curve_fit(func, data[:,0], data[:,1], [1.0, 0])[0] + return slope, offset, func(data[:,0], slope, offset) + +def fit_sawtooth(data, min_val, max_val, sigma=10): + """ + Fits the data to a sawtooth function. + Returns the average absolute error and the number of outliers. + The sample data must span at least one full period. + data is expected to contain one row (t, y) for each sample. + """ + + # Sawtooth function with free parameters for period and x-shift + func = lambda x, a, b: np.mod(a * x + b, max_val - min_val) + min_val + + # Fit period and x-shift + mid_point = (min_val + max_val) / 2 + filtered_data = scipy.ndimage.filters.gaussian_filter(data[:,1], sigma=sigma) + if max_val > min_val: + zero_crossings = data[np.where((filtered_data[:-1] > mid_point) & (filtered_data[1:] < mid_point))[0], 0] + else: + zero_crossings = data[np.where((filtered_data[:-1] < mid_point) & (filtered_data[1:] > mid_point))[0], 0] + + + if len(zero_crossings) == 0: + # No zero-crossing - fit simple line + slope, offset, _ = fit_line(data) + + elif len(zero_crossings) == 1: + # One zero-crossing - fit line based on the longer half + z_index = np.where(data[:,0] > zero_crossings[0])[0][0] + if z_index > len(data[:,0]): + slope, offset, _ = fit_line(data[:z_index]) + else: + slope, offset, _ = fit_line(data[z_index:]) + + else: + # Two or more zero-crossings - determine period based on average distance between zero-crossings + + period = (zero_crossings[1:] - zero_crossings[:-1]).mean() + slope = (max_val - min_val) / period + + #shift = scipy.optimize.curve_fit(lambda x, b: func(x, period, b), data[:,0], data[:,1], [0.0])[0][0] + if np.std(np.mod(zero_crossings, period)) < np.std(np.mod(zero_crossings + period/2, period)): + shift = np.mean(np.mod(zero_crossings, period)) + else: + shift = np.mean(np.mod(zero_crossings + period/2, period)) - period/2 + offset = -slope * shift + + return slope, offset, func(data[:,0], slope, offset) + +def test_curve_fit(data, fitted_curve, max_mean_err, inlier_range, max_outliers): + diffs = data[:,1] - fitted_curve + + mean_err = np.abs(diffs).mean() + if mean_err > max_mean_err: + save_log(np.concatenate([data, np.array([fitted_curve]).transpose()], 1)) + raise TestFailed("curve fit has too large mean error: {} > {}".format(mean_err, max_mean_err)) + + outliers = np.count_nonzero((diffs > inlier_range) | (diffs < -inlier_range)) + if outliers > max_outliers: + save_log(np.concatenate([data, np.array([fitted_curve]).transpose()], 1)) + raise TestFailed("curve fit has too many outliers (err > {}): {} > {}".format(inlier_range, outliers, max_outliers)) + +def test_watchdog(axis, feed_func, logger: Logger): + """ + Tests the watchdog of one axis, using the provided function to feed the watchdog. + This test assumes that the testing host has no more than 300ms random delays. + """ + start = time.monotonic() + axis.config.enable_watchdog = False + axis.error = 0 + axis.config.watchdog_timeout = 1.0 + axis.watchdog_feed() + axis.config.enable_watchdog = True + test_assert_eq(axis.error, 0) + for _ in range(5): # keep the watchdog alive for 3.5 seconds + time.sleep(0.7) + logger.debug('feeding watchdog at {}s'.format(time.monotonic() - start)) + feed_func() + err = axis.error + logger.debug('checking error at {}s'.format(time.monotonic() - start)) + test_assert_eq(err, 0) + + logger.debug('letting watchdog expire...') + time.sleep(1.3) # let the watchdog expire + test_assert_eq(axis.error, AXIS_ERROR_WATCHDOG_TIMER_EXPIRED) + +# Test Components -------------------------------------------------------------# + +class Component(object): + def __init__(self, parent): + self.parent = parent + +class ODriveComponent(Component): + def __init__(self, yaml: dict): + self.handle = None + self.yaml = yaml + #self.axes = [ODriveAxisComponent(None), ODriveAxisComponent(None)] + self.encoders = [ODriveEncoderComponent(self, 0, yaml['encoder0']), ODriveEncoderComponent(self, 1, yaml['encoder1'])] + self.axes = [ODriveAxisComponent(self, 0, yaml['motor0']), ODriveAxisComponent(self, 1, yaml['motor1'])] + for i in range(1,9): + self.__setattr__('gpio' + str(i), Component(self)) + self.can = Component(self) + self.sck = Component(self) + self.miso = Component(self) + self.mosi = Component(self) + + def get_subcomponents(self): + for enc_ctx in self.encoders: + yield 'encoder' + str(enc_ctx.num), enc_ctx + for axis_ctx in self.axes: + yield 'axis' + str(axis_ctx.num), axis_ctx + for i in range(1,9): + yield ('gpio' + str(i)), getattr(self, 'gpio' + str(i)) + yield 'can', self.can + yield 'spi.sck', self.sck + yield 'spi.miso', self.miso + yield 'spi.mosi', self.mosi + + def prepare(self, logger: Logger): + """ + Connects to the ODrive + """ + if not self.handle is None: + return + + logger.debug('waiting for {} ({})'.format(self.yaml['name'], self.yaml['serial-number'])) + self.handle = odrive.find_any( + path="usb", serial_number=self.yaml['serial-number'], timeout=60)#, printer=print) + assert(self.handle) + #for axis_idx, axis_ctx in enumerate(self.axes): + # axis_ctx.handle = self.handle.__dict__['axis{}'.format(axis_idx)] + for encoder_idx, encoder_ctx in enumerate(self.encoders): + encoder_ctx.handle = self.handle.__dict__['axis{}'.format(encoder_idx)].encoder + # TODO: distinguish between axis and motor context + for axis_idx, axis_ctx in enumerate(self.axes): + axis_ctx.handle = self.handle.__dict__['axis{}'.format(axis_idx)] + + def unuse_gpios(self): + self.handle.config.enable_uart = False + self.handle.axis0.config.enable_step_dir = False + self.handle.axis1.config.enable_step_dir = False + self.handle.config.gpio1_pwm_mapping.endpoint = None + self.handle.config.gpio2_pwm_mapping.endpoint = None + self.handle.config.gpio3_pwm_mapping.endpoint = None + self.handle.config.gpio4_pwm_mapping.endpoint = None + self.handle.config.gpio3_analog_mapping.endpoint = None + self.handle.config.gpio4_analog_mapping.endpoint = None + + def save_config_and_reboot(self): + self.handle.save_configuration() + try: + self.handle.reboot() + except fibre.ChannelBrokenException: + pass # this is expected + self.handle = None + time.sleep(2) + self.prepare(logger) + + def erase_config_and_reboot(self): + try: + self.handle.erase_configuration() + except fibre.ChannelBrokenException: + pass # this is expected + self.handle = None + time.sleep(2) + self.prepare(logger) + +class MotorComponent(Component): + def __init__(self, yaml: dict): + self.yaml = yaml + + def prepare(self, logger: Logger): + pass + +class ODriveAxisComponent(Component): + def __init__(self, parent: ODriveComponent, num: int, yaml: dict): + Component.__init__(self, parent) + self.handle = None + self.yaml = yaml # TODO: this is bad naming + self.num = num + + def prepare(self, logger: Logger): + self.parent.prepare(logger) + +class ODriveEncoderComponent(Component): + def __init__(self, parent: ODriveComponent, num: int, yaml: dict): + Component.__init__(self, parent) + self.handle = None + self.yaml = yaml + self.num = num + self.z = Component(self) + self.a = Component(self) + self.b = Component(self) + + def get_subcomponents(self): + return [('z', self.z), ('a', self.a), ('b', self.b)] + + def prepare(self, logger: Logger): + self.parent.prepare(logger) + +class EncoderComponent(Component): + def __init__(self, parent: Component, yaml: dict): + Component.__init__(self, parent) + self.yaml = yaml + self.z = Component(self) + self.a = Component(self) + self.b = Component(self) + + def get_subcomponents(self): + return [('z', self.z), ('a', self.a), ('b', self.b)] + +class GeneralPurposeComponent(Component): + def __init__(self, yaml: dict): + self.components = {} + for component_yaml in yaml.get('components', []): + if component_yaml['type'] == 'can': + self.components[component_yaml['name']] = CanInterfaceComponent(self, component_yaml) + if component_yaml['type'] == 'uart': + self.components[component_yaml['name']] = SerialPortComponent(self, component_yaml) + if component_yaml['type'] == 'gpio': + self.components['gpio' + str(component_yaml['num'])] = LinuxGpioComponent(self, component_yaml) + + def get_subcomponents(self): + return self.components.items() + +class LinuxGpioComponent(Component): + def __init__(self, parent: Component, yaml: dict): + Component.__init__(self, parent) + self.num = int(yaml['num']) + + def config(self, output: bool): + with open("/sys/class/gpio/gpio{}/direction".format(self.num), "w") as fp: + fp.write('out' if output else '0') + + def write(self, state: bool): + with open("/sys/class/gpio/gpio{}/value".format(self.num), "w") as fp: + fp.write('1' if state else '0') + + +class SerialPortComponent(Component): + def __init__(self, parent: Component, yaml: dict): + Component.__init__(self, parent) + self.yaml = yaml + + def get_subcomponents(self): + yield 'tx', Component(self) + yield 'rx', Component(self) + + def open(self, baudrate: int): + import serial + return serial.Serial(self.yaml['port'], baudrate, timeout=1) + +class CanInterfaceComponent(Component): + def __init__(self, parent: Component, yaml: dict): + Component.__init__(self, parent) + self.handle = None + self.yaml = yaml + + def prepare(self, logger: Logger): + if not self.handle is None: + return + + import can + self.handle = can.interface.Bus(bustype='socketcan', channel=self.yaml['interface'], bitrate=250000) + +class TeensyGpio(Component): + def __init__(self, parent: Component, num: int): + Component.__init__(self, parent) + self.num = num + +class TeensyComponent(Component): + def __init__(self, testrig, yaml: dict): + self.testrig = testrig + self.yaml = yaml + self.gpios = [TeensyGpio(self, i) for i in range(24)] + self.routes = [] + self.previous_routes = object() + + def get_subcomponents(self): + for i, gpio in enumerate(self.gpios): + yield ('gpio' + str(i)), gpio + yield 'program', Component(self) + + def add_route(self, input: TeensyGpio, output: TeensyGpio, noise_enable: TeensyGpio): + self.routes.append((input, output, noise_enable)) + + def commit_routing_config(self, logger: Logger): + if self.previous_routes == self.routes: + self.routes = [] + return + + code = '' + code += 'bool noise = false;\n' + code += 'void setup() {\n' + for i, o, n in self.routes: + code += ' pinMode({}, OUTPUT);\n'.format(o.num) + code += '}\n' + code += 'void loop() {\n' + code += ' noise = !noise;\n' + for i, o, n in self.routes: + if n: + # with noise enable + code += ' digitalWrite({}, digitalRead({}) ? noise : digitalRead({}));\n'.format(o.num, n.num, i.num) + else: + # no noise enable + code += ' digitalWrite({}, digitalRead({}));\n'.format(o.num, i.num) + code += '}\n' + + self.compile_and_program(code) + + self.previous_routes = self.routes + self.routes = [] + + def compile(self, sketchfile, hexfile): + env = os.environ.copy() + env['ARDUINO_COMPILE_DESTINATION'] = hexfile + run_shell( + ['arduino', '--board', 'teensy:avr:teensy40', '--verify', sketchfile], + logger, env = env, timeout = 120) + + def program(self, hex_file_path: str, logger: Logger): + """ + Programs the specified hex file onto the Teensy. + To reset the Teensy, a GPIO of the local system must be connected to the + Teensy's "Program" pin. + """ + + # todo: this should be treated like a regular setup resource + program_gpio = self.testrig.get_directly_connected_components(self.testrig.get_component_name(self) + '.program')[0] + + # Put Teensy into program mode by pulling it's program pin down + program_gpio.config(output = True) + program_gpio.write(False) + time.sleep(0.1) + program_gpio.write(True) + + run_shell(["teensy-loader-cli", "-mmcu=imxrt1062", "-w", hex_file_path], logger, timeout = 5) + time.sleep(0.5) # give it some time to boot + + def compile_and_program(self, code: str): + with tempfile.TemporaryDirectory() as temp_dir: + with open(os.path.join(temp_dir, 'code.ino'), 'w+') as code_fp: + code_fp.write(code) + code_fp.flush() + code_fp.seek(0) + print('Writing code to teensy: ') + print(code_fp.read()) + with tempfile.NamedTemporaryFile(suffix='.hex') as hex_fp: + self.compile(code_fp.name, hex_fp.name) + self.program(hex_fp.name, logger) + +class LowPassFilterComponent(Component): + def __init__(self, parent: Component): + Component.__init__(self, parent) + self.en = Component(self) + + def get_subcomponents(self): + yield 'en', self.en + + +class ProxiedComponent(Component): + def __init__(self, impl, *gpio_tuples): + """ + Each element in gpio_tuples should be a tuple of the form: + (teensy: TeensyComponent, gpio_in, gpio_out, gpio_noise_enable) + """ + Component.__init__(self, getattr(impl, 'parent', None)) + self.impl = impl + assert(all([len(t) == 4 for t in gpio_tuples])) + self.gpio_tuples = list(gpio_tuples) + + def __repr__(self): + return testrig.get_component_name(self.impl) + ' (routed via ' + ', '.join((testrig.get_component_name(t) + ': ' + str(i.num) + ' => ' + str(o.num)) for t, i, o, n in self.gpio_tuples) + ')' + + def __eq__(self, obj): + return isinstance(obj, ProxiedComponent) and (self.impl == obj.impl) # and (self.gpio_tuples == obj.gpio_tuples) + + def prepare(self): + for teensy, gpio_in, gpio_out, gpio_noise_enable in self.gpio_tuples: + teensy.add_route(gpio_in, gpio_out, gpio_noise_enable) + +class TestRig(): + def __init__(self, yaml: dict, logger: Logger): + # Contains all components (including subcomponents). + # Ports are components too. + self.components_by_name = {} # {'name': object, ...} + self.names_by_component = {} # {'name': object, ...} + + def add_component(name, component): + self.components_by_name[name] = component + self.names_by_component[component] = name + if hasattr(component, 'get_subcomponents'): + for subname, subcomponent in component.get_subcomponents(): + add_component(name + '.' + subname, subcomponent) + + for component_yaml in yaml['components']: + if component_yaml['type'] == 'odrive': + add_component(component_yaml['name'], ODriveComponent(component_yaml)) + elif component_yaml['type'] == 'generalpurpose': + add_component(component_yaml['name'], GeneralPurposeComponent(component_yaml)) + elif component_yaml['type'] == 'teensy': + add_component(component_yaml['name'], TeensyComponent(self, component_yaml)) + elif component_yaml['type'] == 'motor': + add_component(component_yaml['name'], MotorComponent(component_yaml)) + elif component_yaml['type'] == 'encoder': + add_component(component_yaml['name'], EncoderComponent(self, component_yaml)) + elif component_yaml['type'] == 'lpf': + add_component(component_yaml['name'], LowPassFilterComponent(self)) + else: + logger.warn('test rig has unsupported component ' + component_yaml['type']) + continue + + # List of disjunct sets, where each set holds references of the mutually connected components + self.connections = [] + for connection_yaml in yaml['connections']: + self.connections.append(set(self.components_by_name[name] for name in connection_yaml)) + self.connections = list(disjoint_sets(self.connections)) + + # Dict for fast lookup of the connection sets for each port + self.net_by_component = {} + for s in self.connections: + for port in s: + self.net_by_component[port] = s + + def get_components(self, t: type): + """Returns a tuple (name, component) for all components that are of the specified type""" + return (comp for comp in self.names_by_component.keys() if isinstance(comp, t)) + + def get_component_name(self, component: Component): + if isinstance(component, ProxiedComponent): + return self.names_by_component[component.impl] + else: + return self.names_by_component[component] + + def get_directly_connected_components(self, component: Union[str, Component]): + """ + Returns all components that are directly connected to the specified + component, excluding the specified component itself. + """ + if isinstance(component, str): + component = self.components_by_name[component] + result = self.net_by_component.get(component, set([component])) + return [c for c in result if (c != component)] + + def get_connected_components(self, src: Union[dict, Tuple[Union[Component, str], bool]], comp_type: type = None): + """ + Returns all components that are either directly or indirectly (through a + Teensy) connected to the specified component(s). + + component: Either: + - A component object. + - A component name given as string. + - A tuple of the form (comp, dir) where comp is a component object + or name and dir specifies the data direction. + The direction is required if routing through a Teensy should be + considered. + - A dict {sumcomponent: val} where subcomponent is a string + such as 'tx' or 'rx' and val is of one of the forms described above. + + A type can be specified to filter the connected components. + """ + + if isinstance(src, dict): + component_list = [] + for name, subsrc in src.items(): + component_list.append([c for c in self.get_connected_components(subsrc) if self.get_component_name(c).endswith('.' + name)]) + + for combination in itertools.product(*component_list): + if len(set(c.parent for c in combination)) != 1: + continue # parent of the components don't match + proxied_dst = combination[0].parent + if comp_type and not isinstance(proxied_dst, comp_type): + continue # not the requested type + gpio_tuples = [c2 for c in combination for c2 in c.gpio_tuples if isinstance(c, ProxiedComponent)] + if len(gpio_tuples): + yield ProxiedComponent(proxied_dst, *gpio_tuples) + else: + yield proxied_dst + + else: + + if isinstance(src, tuple): + src, dir = src + else: + dir = None + + for dst in self.get_directly_connected_components(src): + if (not comp_type) or isinstance(dst, comp_type): + yield dst + + if (not dir is None) and isinstance(getattr(dst, 'parent', None), TeensyComponent): + teensy = dst.parent + for gpio2 in teensy.gpios: + for proxied_dst in self.get_directly_connected_components(gpio2): + if (not comp_type) or isinstance(proxied_dst, comp_type): + yield ProxiedComponent(proxied_dst, (teensy, dst if dir else gpio2, gpio2 if dir else dst, None)) + + +# Helper functions ------------------------------------------------------------# + +def request_state(axis_ctx: ODriveAxisComponent, state, expect_success=True): + axis_ctx.handle.requested_state = state + time.sleep(0.001) + if expect_success: + test_assert_eq(axis_ctx.handle.current_state, state) + else: + test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE) + test_assert_eq(axis_ctx.handle.error, AXIS_ERROR_INVALID_STATE) + axis_ctx.handle.error = AXIS_ERROR_NONE # reset error + +def get_errors(axis_ctx: ODriveAxisComponent): + errors = [] + if axis_ctx.handle.motor.error != 0: + errors.append("motor failed with error 0x{:04X}".format(axis_ctx.handle.motor.error)) + if axis_ctx.handle.encoder.error != 0: + errors.append("encoder failed with error 0x{:04X}".format(axis_ctx.handle.encoder.error)) + if axis_ctx.handle.sensorless_estimator.error != 0: + errors.append("sensorless_estimator failed with error 0x{:04X}".format(axis_ctx.handle.sensorless_estimator.error)) + if axis_ctx.handle.error != 0: + errors.append("axis failed with error 0x{:04X}".format(axis_ctx.handle.error)) + elif len(errors) > 0: + errors.append("and by the way: axis reports no error even though there is one") + return errors + +def test_assert_no_error(axis_ctx: ODriveAxisComponent): + errors = get_errors(axis_ctx) + if len(errors) > 0: + raise TestFailed("\n".join(errors)) + +def run_shell(command_line, logger, env=None, timeout=None): + """ + Runs a shell command in the current directory + """ + import shlex + import subprocess + logger.debug("invoke: " + str(command_line)) + if isinstance(command_line, list): + cmd = command_line + else: + cmd = shlex.split(command_line) + result = subprocess.run(cmd, timeout=timeout, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env) + if result.returncode != 0: + logger.error(result.stdout.decode(sys.stdout.encoding)) + raise TestFailed("command {} failed".format(command_line)) + + +def get_combinations(param_options): + if isinstance(param_options, tuple): + if len(param_options) > 0: + for part1, part2 in itertools.product( + get_combinations(param_options[0]), + get_combinations(param_options[1:]) if (len(param_options) > 1) else [()]): + assert(isinstance(part1, tuple)) + assert(isinstance(part2, tuple)) + yield part1 + part2 + elif is_list_like(param_options): + for item in param_options: + for c in get_combinations(item): + yield c + else: + yield (param_options,) + +def select_params(param_options): + # Select parameters from the resource list + # (this could be arbitrarily complex to improve parallelization of the tests) + for combination in get_combinations(param_options): + if all_unique([x for x in combination if isinstance(x, Component)]): + return list(combination) + + return None + +def run(tests): + if not isinstance(tests, list): + tests = [tests] + + for test in tests: + # The result of get_test_cases can be described in ABNF grammar: + # test-case-list = *arglist + # arglist = *flexible-arg + # flexible-arg = component / *argvariant + # argvariant = component / arglist + # + # If for a particular test-case, the components are not given plainly + # but in some selectable form, the test driver will select exactly one + # of those options. + # In other words, it will bring arglist from the form *flexible-arg + # into the form *component before calling the test. + # + # All of the provided test-cases are executed. If none is provided, + # a warning is reported. A warning is also reported if for a particular + # test case no component combination can be resolved. + + test_cases = list(test.get_test_cases(testrig)) + + if len(test_cases) == 0: + logger.warn('no test cases are available to conduct the test {}'.format(type(test).__name__)) + continue + + for test_case in test_cases: + params = select_params(test_case) + if params is None: + logger.warn('no resources are available to conduct the test {}'.format(type(test).__name__)) + continue + + logger.notify('* preparing {} with {}...'.format(type(test).__name__, + [(testrig.get_component_name(p) if isinstance(p, Component) else str(p)) for p in params])) + + teensies = set() + for param in params: + if isinstance(param, ProxiedComponent): + param.prepare() + for teensy, _, _, _ in param.gpio_tuples: + teensies.add(teensy) + + for teensy in teensies: + teensy.commit_routing_config(logger) + + # prepare all components + teensies = set() + for param in params: + if isinstance(param, ProxiedComponent): + continue + if hasattr(param, 'prepare'): + param.prepare(logger) + + logger.notify('* running {} on {}...'.format(type(test).__name__, + [(testrig.get_component_name(p) if isinstance(p, Component) else str(p)) for p in params])) + + # Resolve routed components + for i, param in enumerate(params): + if isinstance(param, ProxiedComponent): + params[i] = param.impl + + test.run_test(*params, logger) + + + logger.success('All tests passed!') + + +# Load test engine ------------------------------------------------------------# + +# Parse arguments +parser = argparse.ArgumentParser(description='ODrive automated test tool\n') +parser.add_argument("--ignore", metavar='DEVICE', action='store', nargs='+', + help="Ignore (disable) one or more components of the test rig") + # TODO: implement +parser.add_argument("--test-rig-yaml", type=argparse.FileType('r'), required=True, + help="test rig YAML file") +parser.add_argument("--setup-host", action='store_true', default=False, + help="configure operating system functions such as GPIOs (requires root)") +parser.set_defaults(ignore=[]) + +args = parser.parse_args() + +# Load objects +test_rig_yaml = yaml.load(args.test_rig_yaml, Loader=yaml.BaseLoader) +logger = Logger() + +testrig = TestRig(test_rig_yaml, logger) + + +if args.setup_host: + for gpio in testrig.get_components(LinuxGpioComponent): + num = gpio.num + logger.debug('exporting GPIO ' + str(num) + ' to user space...') + if not os.path.isdir("/sys/class/gpio/gpio{}".format(num)): + with open("/sys/class/gpio/export", "w") as fp: + fp.write(str(num)) + os.chmod("/sys/class/gpio/gpio{}/value".format(num), stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + os.chmod("/sys/class/gpio/gpio{}/direction".format(num), stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + + for port in testrig.get_components(SerialPortComponent): + logger.debug('changing permissions on ' + port.yaml['port'] + '...') + os.chmod(port.yaml['port'], stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + + if len(list(testrig.get_components(TeensyComponent))): + # This breaks the annoying teensy loader that shows up on every compile + logger.debug('modifying teensyduino installation...') + if not os.path.isfile('/usr/share/arduino/hardware/tools/teensy_post_compile_old'): + os.rename('/usr/share/arduino/hardware/tools/teensy_post_compile', '/usr/share/arduino/hardware/tools/teensy_post_compile_old') + with open('/usr/share/arduino/hardware/tools/teensy_post_compile', 'w') as scr: + scr.write('#!/usr/bin/env bash\n') + scr.write('if [ "$ARDUINO_COMPILE_DESTINATION" != "" ]; then\n') + scr.write(' cp -r ${2#-path=}/*.ino.hex ${ARDUINO_COMPILE_DESTINATION}\n') + scr.write('fi\n') + os.chmod('/usr/share/arduino/hardware/tools/teensy_post_compile', stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + + # Bring up CAN interface(s) + for intf in testrig.get_components(CanInterfaceComponent): + name = intf.yaml['interface'] + logger.debug('bringing up {}...'.format(name)) + run_shell('ip link set dev {} down'.format(name), logger) + run_shell('ip link set dev {} type can bitrate 250000'.format(name), logger) + run_shell('ip link set dev {} type can loopback off'.format(name), logger) + run_shell('ip link set dev {} up'.format(name), logger) + diff --git a/tools/odrive/tests/uart_ascii_test.py b/tools/odrive/tests/uart_ascii_test.py new file mode 100644 index 00000000..ff5a41de --- /dev/null +++ b/tools/odrive/tests/uart_ascii_test.py @@ -0,0 +1,301 @@ + +import test_runner + +import struct +import time +import os +import io +import functools +import operator + +from fibre.utils import Logger +from odrive.enums import * +from test_runner import * + + +def append_checksum(command): + return command + b'*' + str(functools.reduce(operator.xor, command)).encode('ascii') + +def strip_checksum(command): + command, _, checksum = command.partition(b'*') + test_assert_eq(int(checksum.strip()), functools.reduce(operator.xor, command)) + return command + +def reset_state(ser): + """Resets the state of the ASCII protocol by flushing all buffers""" + ser.flushOutput() # ensure that all previous bytes are sent + time.sleep(0.1) # wait for ODrive to handle last input (buffer might be full) + ser.write(b'\n') # terminate line + ser.flushOutput() # ensure that end-of-line is sent + time.sleep(0.1) # wait for any response that this may generate + ser.flushInput() # discard response + +class TestUartAscii(): + """ + Tests the most important functions of the ASCII protocol. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + ports = list(testrig.get_connected_components({ + 'rx': (odrive.gpio1, True), + 'tx': (odrive.gpio2, False) + }, SerialPortComponent)) + yield (odrive, ports) + + def run_test(self, odrive: ODriveComponent, port: SerialPortComponent, logger: Logger): + logger.debug('Enabling UART...') + # GPIOs might be in use by something other than UART and some components + # might be configured so that they would fail in the later test. + odrive.erase_config_and_reboot() + odrive.handle.config.enable_uart = True + + with port.open(115200) as ser: + # reset port to known state + reset_state(ser) + + # Read a top-level attribute + ser.write(b'r vbus_voltage\n') + response = float(ser.readline().strip()) + test_assert_eq(response, odrive.handle.vbus_voltage, accuracy=0.1) + + # Read an unknown attribute + ser.write(b'r blahblah\n') + response = ser.readline().strip() + test_assert_eq(response, b'invalid property') + + # Send command with delays in between + for byte in b'r vbus_voltage\n': + ser.write([byte]) + time.sleep(0.1) + response = float(ser.readline().strip()) + test_assert_eq(response, odrive.handle.vbus_voltage, accuracy=0.1) + + # Test GCode checksum and comments + ser.write(b'r vbus_voltage *12\n') # invalid checksum + test_assert_eq(ser.readline(), b'') + ser.write(append_checksum(b'r vbus_voltage ') + b' ; this is a comment\n') # valid checksum + response = float(strip_checksum(ser.readline()).strip()) + test_assert_eq(response, odrive.handle.vbus_voltage, accuracy=0.1) + + # Read an attribute with a long name + ser.write(b'r axis0.motor.current_control.v_current_control_integral_d\n') + response = float(ser.readline().strip()) + test_assert_eq(response, odrive.handle.axis0.motor.current_control.v_current_control_integral_d, accuracy=0.1) + + # Write an attribute + ser.write(b'w test_property 12345\n') + ser.write(b'r test_property\n') + response = int(ser.readline().strip()) + test_assert_eq(response, 12345) + + # Test custom setter (aka property write hook) + odrive.handle.axis0.motor.config.phase_resistance = 1 + odrive.handle.axis0.motor.config.phase_inductance = 1 + odrive.handle.axis0.motor.config.current_control_bandwidth = 1000 + old_gain = odrive.handle.axis0.motor.current_control.p_gain + test_assert_eq(old_gain, 1000, accuracy=0.0001) # must be non-zero for subsequent check to work + ser.write('w axis0.motor.config.current_control_bandwidth {}\n'.format(odrive.handle.axis0.motor.config.current_control_bandwidth / 2).encode('ascii')) + test_assert_eq(ser.readline(), b'') + test_assert_eq(odrive.handle.axis0.motor.current_control.p_gain, old_gain / 2, accuracy=0.0001) + + # Test 'c', 'v', 'p', 'q' and 'f' commands + + odrive.handle.axis0.controller.input_torque = 0 + ser.write(b'c 0 12.5\n') + test_assert_eq(ser.readline(), b'') + test_assert_eq(odrive.handle.axis0.controller.input_torque, 12.5, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_TORQUE_CONTROL) + + odrive.handle.axis0.controller.input_vel = 0 + odrive.handle.axis0.controller.input_torque = 0 + ser.write(b'v 0 567.8 12.5\n') + test_assert_eq(ser.readline(), b'') + test_assert_eq(odrive.handle.axis0.controller.input_vel, 567.8, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.input_torque, 12.5, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_VELOCITY_CONTROL) + + odrive.handle.axis0.controller.input_pos = 0 + odrive.handle.axis0.controller.input_vel = 0 + odrive.handle.axis0.controller.input_torque = 0 + ser.write(b'p 0 123.4 567.8 12.5\n') + test_assert_eq(ser.readline(), b'') + test_assert_eq(odrive.handle.axis0.controller.input_pos, 123.4, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.input_vel, 567.8, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.input_torque, 12.5, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_POSITION_CONTROL) + + odrive.handle.axis0.controller.input_pos = 0 + odrive.handle.axis0.controller.config.vel_limit = 0 + odrive.handle.axis0.motor.config.current_lim = 0 + ser.write(b'q 0 123.4 567.8 12.5\n') + test_assert_eq(ser.readline(), b'') + test_assert_eq(odrive.handle.axis0.controller.input_pos, 123.4, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.config.vel_limit, 567.8, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.motor.config.torque_lim, 12.5, accuracy=0.001) + test_assert_eq(odrive.handle.axis0.controller.config.control_mode, CONTROL_MODE_POSITION_CONTROL) + + ser.write(b'f 0\n') + response = ser.readline().strip() + test_assert_eq(float(response.split()[0]), odrive.handle.axis0.encoder.pos_estimate, accuracy=0.001) + test_assert_eq(float(response.split()[1]), odrive.handle.axis0.encoder.vel_estimate, accuracy=0.001) + + test_watchdog(odrive.handle.axis0, lambda: ser.write(b'u 0\n'), logger) + test_assert_eq(ser.readline(), b'') # check if the device remained silent during the test + + + # TODO: test cases for 't', 'ss', 'se', 'sr' commands + + +class TestUartBaudrate(): + """ + Tests if the UART baudrate setting works as intended. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + ports = list(testrig.get_connected_components({ + 'rx': (odrive.gpio1, True), + 'tx': (odrive.gpio2, False) + }, SerialPortComponent)) + yield (odrive, ports) + + def run_test(self, odrive: ODriveComponent, port: SerialPortComponent, logger: Logger): + odrive.handle.axis0.config.enable_step_dir = False + odrive.handle.config.enable_uart = True + + odrive.handle.config.uart_baudrate = 9600 + odrive.save_config_and_reboot() + + # Control test: talk to the ODrive with the wrong baudrate + with port.open(115200) as ser: + # reset port to known state + reset_state(ser) + + ser.write(b'r vbus_voltage\n') + test_assert_eq(ser.readline().strip(), b'') + + with port.open(9600) as ser: + # reset port to known state + reset_state(ser) + + # Check if protocol works + ser.write(b'r vbus_voltage\n') + response = float(ser.readline().strip()) + test_assert_eq(response, odrive.handle.vbus_voltage, accuracy=0.1) + + odrive.handle.config.uart_baudrate = 115200 + odrive.save_config_and_reboot() + + +class TestUartBurnIn(): + """ + Tests if the ASCII protocol can handle 64kB of random data being thrown at it. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + ports = list(testrig.get_connected_components({ + 'rx': (odrive.gpio1, True), + 'tx': (odrive.gpio2, False) + }, SerialPortComponent)) + yield (odrive, ports) + + def run_test(self, odrive: ODriveComponent, port: SerialPortComponent, logger: Logger): + odrive.handle.axis0.config.enable_step_dir = False + odrive.handle.config.enable_uart = True + + with port.open(115200) as ser: + with open('/dev/random', 'rb') as rand: + buf = rand.read(65536) + ser.write(buf) + + # reset port to known state + reset_state(ser) + + # Check if protocol still works + ser.write(b'r vbus_voltage\n') + response = float(ser.readline().strip()) + test_assert_eq(response, odrive.handle.vbus_voltage, accuracy=0.1) + + +class TestUartNoise(): + """ + Tests if the UART can handle invalid signals. + """ + + def get_test_cases(self, testrig: TestRig): + for odrive in testrig.get_components(ODriveComponent): + # For every ODrive, find a connected serial port which has a teensy + # in between, so that we can inject noise, + + ports = list(testrig.get_connected_components({ + 'rx': (odrive.gpio1, True), + 'tx': (odrive.gpio2, False) + }, SerialPortComponent)) + + # Hack the bus objects to enable noise_enable functionality on the TX line. + + def get_noise_gpio(bus): + teensy = bus.gpio_tuples[1][0] + for teensy_gpio in teensy.gpios: + for other_gpio in testrig.get_directly_connected_components(teensy_gpio): + if isinstance(other_gpio, LinuxGpioComponent): + return teensy_gpio, other_gpio + return None + + for idx, bus in enumerate(ports): + noise_gpio_on_teensy, noise_gpio_on_rpi = get_noise_gpio(bus) + assert(noise_gpio_on_rpi) + t, i, o, _ = bus.gpio_tuples[1] + bus.gpio_tuples[1] = (t, i, o, noise_gpio_on_teensy) + ports[idx] = (bus, noise_gpio_on_rpi) + + yield (odrive, ports) + + def run_test(self, odrive: ODriveComponent, port: SerialPortComponent, noise_enable: LinuxGpioComponent, logger: Logger): + noise_enable.config(output=True) + noise_enable.write(False) + time.sleep(0.1) + + odrive.handle.axis0.config.enable_step_dir = False + odrive.handle.config.enable_uart = True + + with port.open(115200) as ser: + # reset port to known state + reset_state(ser) + + # Enable square wave of ~1.6MHz on the ODrive's RX line + noise_enable.write(True) + + time.sleep(0.1) + reset_state(ser) + + time.sleep(1.0) + + # Read an attribute (should fail because the command is not passed through) + ser.write(b'r vbus_voltage\n') + test_assert_eq(ser.readline(), b'') + + # Disable square wave + noise_enable.write(False) + + # Give receiver some time to recover + time.sleep(0.1) + + # reset port to known state + reset_state(ser) + + # Try again + ser.write(b'r vbus_voltage\n') + response = float(ser.readline().strip()) + test_assert_eq(response, odrive.handle.vbus_voltage, accuracy=0.1) + + +if __name__ == '__main__': + test_runner.run([ + TestUartAscii(), + TestUartBaudrate(), + TestUartBurnIn(), + TestUartNoise(), + ]) diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index f5ce0c7f..571db8e9 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -6,8 +6,11 @@ import threading import platform import subprocess import os +import numpy as np +import matplotlib.pyplot as plt from fibre.utils import Event -from odrive.enums import errors +import odrive.enums +from odrive.enums import * try: if platform.system() == 'Windows': @@ -28,9 +31,41 @@ _VT100Colors = { 'default': '\x1b[0m' } +def calculate_thermistor_coeffs(degree, Rload, R_25, Beta, Tmin, Tmax, plot = False): + T_25 = 25 + 273.15 #Kelvin + temps = np.linspace(Tmin, Tmax, 1000) + tempsK = temps + 273.15 + + # https://en.wikipedia.org/wiki/Thermistor#B_or_%CE%B2_parameter_equation + r_inf = R_25 * np.exp(-Beta/T_25) + R_temps = r_inf * np.exp(Beta/tempsK) + V = Rload / (Rload + R_temps) + + fit = np.polyfit(V, temps, degree) + p1 = np.poly1d(fit) + fit_temps = p1(V) + + if plot: + print(fit) + plt.plot(V, temps, label='actual') + plt.plot(V, fit_temps, label='fit') + plt.xlabel('normalized voltage') + plt.ylabel('Temp [C]') + plt.legend(loc=0) + plt.show() + + return p1 + class OperationAbortedException(Exception): pass +def set_motor_thermistor_coeffs(axis, Rload, R_25, Beta, Tmin, TMax): + coeffs = calculate_thermistor_coeffs(3, Rload, R_25, Beta, Tmin, TMax) + axis.motor_thermistor.config.poly_coefficient_0 = float(coeffs[3]) + axis.motor_thermistor.config.poly_coefficient_1 = float(coeffs[2]) + axis.motor_thermistor.config.poly_coefficient_2 = float(coeffs[1]) + axis.motor_thermistor.config.poly_coefficient_3 = float(coeffs[0]) + def dump_errors(odrv, clear=False): axes = [(name, axis) for name, axis in odrv._remote_attributes.items() if 'axis' in name] axes.sort() @@ -40,27 +75,36 @@ def dump_errors(odrv, clear=False): # Flatten axis and submodules # (name, remote_obj, errorcode) module_decode_map = [ - ('axis', axis, errors.axis), - ('motor', axis.motor, errors.motor), - ('encoder', axis.encoder, errors.encoder), - ('controller', axis.controller, errors.controller), + ('axis', axis, {k: v for k, v in odrive.enums.__dict__ .items() if k.startswith("AXIS_ERROR_")}), + ('motor', axis.motor, {k: v for k, v in odrive.enums.__dict__ .items() if k.startswith("MOTOR_ERROR_")}), + ('fet_thermistor', axis.fet_thermistor, {k: v for k, v in odrive.enums.__dict__ .items() if k.startswith("THERMISTOR_CURRENT_LIMITER_ERROR")}), + ('motor_thermistor', axis.motor_thermistor, {k: v for k, v in odrive.enums.__dict__ .items() if k.startswith("THERMISTOR_CURRENT_LIMITER_ERROR")}), + ('encoder', axis.encoder, {k: v for k, v in odrive.enums.__dict__ .items() if k.startswith("ENCODER_ERROR_")}), + ('controller', axis.controller, {k: v for k, v in odrive.enums.__dict__ .items() if k.startswith("CONTROLLER_ERROR_")}), ] # Module error decode for name, remote_obj, errorcodes in module_decode_map: prefix = ' '*2 + name + ": " - if (remote_obj.error != errorcodes.ERROR_NONE): + if (remote_obj.error != 0): + foundError = False print(prefix + _VT100Colors['red'] + "Error(s):" + _VT100Colors['default']) - errorcodes_tup = [(name, val) for name, val in errorcodes.__dict__.items() if 'ERROR_' in name] - for codename, codeval in errorcodes_tup: - if remote_obj.error & codeval != 0: - print(" " + codename) + errorcodes_dict = {val: name for name, val in errorcodes.items() if 'ERROR_' in name} + for bit in range(64): + if remote_obj.error & (1 << bit) != 0: + print(" " + errorcodes_dict.get((1 << bit), 'UNKNOWN ERROR: 0x{:08X}'.format(1 << bit))) if clear: - remote_obj.error = errorcodes.ERROR_NONE + remote_obj.error = 0 else: print(prefix + _VT100Colors['green'] + "no error" + _VT100Colors['default']) -data_rate = 10 +def oscilloscope_dump(odrv, num_vals, filename='oscilloscope.csv'): + with open(filename, 'w') as f: + for x in range(num_vals): + f.write(str(odrv.get_oscilloscope_val(x))) + f.write('\n') + +data_rate = 100 plot_rate = 10 num_samples = 1000 def start_liveplotter(get_var_callback): @@ -118,11 +162,115 @@ def start_liveplotter(get_var_callback): plot_t = threading.Thread(target=plot_data) plot_t.daemon = True plot_t.start() - return cancellation_token; #plot_data() + +class BulkCapture: + ''' + Asynchronously captures a bulk set of data when instance is created. + + get_var_callback: a function that returns the data you want to collect (see the example below) + data_rate: Rate in hz + length: Length of time to capture in seconds + + Example Usage: + capture = BulkCapture(lambda :[odrv0.axis0.encoder.pos_estimate, odrv0.axis0.controller.pos_setpoint]) + # Do stuff while capturing (like sending position commands) + capture.event.wait() # When you're done doing stuff, wait for the capture to be completed. + print(capture.data) # Do stuff with the data + capture.plot_data() # Helper method to plot the data + ''' + + def __init__(self, + get_var_callback, + data_rate=500.0, + duration=2.0): + from threading import Event, Thread + import numpy as np + + self.get_var_callback = get_var_callback + self.event = Event() + def loop(): + vals = [] + start_time = time.monotonic() + period = 1.0 / data_rate + while time.monotonic() - start_time < duration: + try: + data = get_var_callback() + except Exception as ex: + print(str(ex)) + print("Waiting 1 second before next data point") + time.sleep(1) + continue + relative_time = time.monotonic() - start_time + vals.append([relative_time] + data) + time.sleep(period - (relative_time % period)) # this ensures consistently timed samples + self.data = np.array(vals) # A lock is not really necessary due to the event + print("Capture complete") + achieved_data_rate = len(self.data) / self.data[-1, 0] + if achieved_data_rate < (data_rate * 0.9): + print("Achieved average data rate: {}Hz".format(achieved_data_rate)) + print("If this rate is significantly lower than what you specified, consider lowering it below the achieved value for more consistent sampling.") + self.event.set() # tell the main thread that the bulk capture is complete + Thread(target=loop, daemon=True).start() + + def plot(self): + import matplotlib.pyplot as plt + import inspect + from textwrap import wrap + plt.plot(self.data[:,0], self.data[:,1:]) + plt.xlabel("Time (seconds)") + title = (str(inspect.getsource(self.get_var_callback)) + .strip("['\\n']") + .split(" = ")[1]) + plt.title("\n".join(wrap(title, 60))) + plt.legend(range(self.data.shape[1]-1)) + plt.show() + + +def step_and_plot( axis, + step_size=100.0, + settle_time=0.5, + data_rate=500.0, + ctrl_mode=CONTROL_MODE_POSITION_CONTROL): + + if ctrl_mode is CONTROL_MODE_POSITION_CONTROL: + get_var_callback = lambda :[axis.encoder.pos_estimate, axis.controller.pos_setpoint] + initial_setpoint = axis.encoder.pos_estimate + def set_setpoint(setpoint): + axis.controller.pos_setpoint = setpoint + elif ctrl_mode is CONTROL_MODE_VELOCITY_CONTROL: + get_var_callback = lambda :[axis.encoder.vel_estimate, axis.controller.vel_setpoint] + initial_setpoint = 0 + def set_setpoint(setpoint): + axis.controller.vel_setpoint = setpoint + else: + print("Invalid control mode") + return + + initial_settle_time = 0.5 + initial_control_mode = axis.controller.config.control_mode # Set it back afterwards + print(initial_control_mode) + axis.controller.config.control_mode = ctrl_mode + axis.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL + + capture = BulkCapture(get_var_callback, + data_rate=data_rate, + duration=initial_settle_time + settle_time) + + set_setpoint(initial_setpoint) + time.sleep(initial_settle_time) + set_setpoint(initial_setpoint + step_size) # relative/incremental movement + + capture.event.wait() # wait for Bulk Capture to be complete + + axis.requested_state = AXIS_STATE_IDLE + axis.controller.config.control_mode = initial_control_mode + capture.plot() + + def print_drv_regs(name, motor): """ Dumps the current gate driver regisers for the specified motor @@ -164,7 +312,7 @@ def rate_test(device): vals.append(device.axis0.loop_counter) loopsPerFrame = (vals[-1] - vals[0])/numFrames - loopsPerSec = (168000000/(2*10192)) + loopsPerSec = (168000000/(6*3500)) FramePerSec = loopsPerSec/loopsPerFrame print("Frames per second: " + str(FramePerSec)) diff --git a/tools/odrive/version.py b/tools/odrive/version.py index a96e9b4e..7a323899 100644 --- a/tools/odrive/version.py +++ b/tools/odrive/version.py @@ -11,8 +11,13 @@ def version_str_to_tuple(version_string): (major, minor, revision, prerelease) Example: "fw-v0.3.6-23" => (0, 3, 6, True) + + If version_string does not match the pattern above, this function throws an + Exception. """ - regex=r'.*v([0-9a-zA-Z]+).([0-9a-zA-Z]+).([0-9a-zA-Z]+)(.*)' + regex=r'.*v([0-9]+)\.([0-9]+)\.([0-9]+)(.*)' + if not re.match(regex, version_string): + raise Exception() return (int(re.sub(regex, r"\1", version_string)), int(re.sub(regex, r"\2", version_string)), int(re.sub(regex, r"\3", version_string)), @@ -74,20 +79,20 @@ if __name__ == '__main__': print('Firmware version {}.{}.{}{} ({})'.format( major, minor, revision, '-dev' if unreleased else '', git_name)) - args.output.write('#define FW_VERSION "{}"\n'.format(git_name)) - args.output.write('#define FW_VERSION_MAJOR {}\n'.format(major)) - args.output.write('#define FW_VERSION_MINOR {}\n'.format(minor)) - args.output.write('#define FW_VERSION_REVISION {}\n'.format(revision)) - args.output.write('#define FW_VERSION_UNRELEASED {}\n'.format(1 if unreleased else 0)) + #args.output.write('const unsigned char fw_version = "{}"\n'.format(git_name)) + args.output.write('const unsigned char fw_version_major_ = {};\n'.format(major)) + args.output.write('const unsigned char fw_version_minor_ = {};\n'.format(minor)) + args.output.write('const unsigned char fw_version_revision_ = {};\n'.format(revision)) + args.output.write('const unsigned char fw_version_unreleased_ = {};\n'.format(1 if unreleased else 0)) def setup_udev_rules(logger): if platform.system() != 'Linux': - logger.error("This command only makes sense on Linux") + if logger: logger.error("This command only makes sense on Linux") return if os.getuid() != 0: - logger.warn("you should run this as root, otherwise it will probably not work") + if logger: logger.warn("you should run this as root, otherwise it will probably not work") with open('/etc/udev/rules.d/91-odrive.rules', 'w') as file: file.write('SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d3[0-9]", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1"\n') subprocess.check_call(["udevadm", "control", "--reload-rules"]) subprocess.check_call(["udevadm", "trigger"]) - logger.info('udev rules configured successfully') + if logger: logger.info('udev rules configured successfully') diff --git a/tools/odrivetool b/tools/odrivetool index 5c3af5b7..60ed99bb 100755 --- a/tools/odrivetool +++ b/tools/odrivetool @@ -8,6 +8,7 @@ import sys import os import argparse import time +import math sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname( os.path.realpath(__file__))), diff --git a/tools/plot_oscilloscope.py b/tools/plot_oscilloscope.py new file mode 100644 index 00000000..2a1809b8 --- /dev/null +++ b/tools/plot_oscilloscope.py @@ -0,0 +1,9 @@ + +from matplotlib import pyplot as plt +import sys + +with open(sys.argv[1]) as f: + data = list(map(float, f)) + +plt.plot(data) +plt.show() \ No newline at end of file diff --git a/tools/setup.py b/tools/setup.py index 986d9d0d..83d5c1ed 100644 --- a/tools/setup.py +++ b/tools/setup.py @@ -32,7 +32,7 @@ to publish packages with the name odrive. """ # Set to true to make the current release -is_release = False +is_release = True # Set to true to make an official post-release, rather than dev of new version is_post_release = False @@ -91,9 +91,8 @@ if creating_package: if not creating_package: import platform if platform.system() == 'Linux': - from fibre.utils import Logger try: - odrive.version.setup_udev_rules(Logger()) + odrive.version.setup_udev_rules(None) except Exception: print("Warning: could not set up udev rules. Run `sudo odrivetool udev-setup` to try again.") @@ -117,6 +116,7 @@ try: 'IntelHex', # Used to by DFU to download firmware from github 'matplotlib', # Required to run the liveplotter 'monotonic', # For compatibility with older python versions + 'appdirs', # Used to find caching directory 'pywin32 >= 222; platform_system == "Windows"' # Required for fancy terminal features on Windows ], package_data={'': ['version.txt']}, diff --git a/tools/setup_hall_as_index.py b/tools/setup_hall_as_index.py index c01de4ff..8ba871a8 100644 --- a/tools/setup_hall_as_index.py +++ b/tools/setup_hall_as_index.py @@ -30,7 +30,7 @@ for ax in axes: ax.encoder.config.find_idx_on_lockin_only = True ax.encoder.config.idx_search_unidirectional = True - ax.controller.config.control_mode = CTRL_MODE_VELOCITY_CONTROL + ax.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL ax.controller.config.vel_limit = 10000 ax.controller.config.vel_gain = 0.002205736003816127 ax.controller.config.vel_integrator_gain = 0.022057360038161278 @@ -45,7 +45,7 @@ for ax in axes: def wait_and_exit_on_error(ax): while ax.current_state != AXIS_STATE_IDLE: time.sleep(0.1) - if ax.error != errors.axis.ERROR_NONE: + if ax.error != AXIS_ERROR_NONE: dump_errors(odrv, True) exit() diff --git a/tools/test-rig-rpi.yaml b/tools/test-rig-rpi.yaml new file mode 100644 index 00000000..b9954049 --- /dev/null +++ b/tools/test-rig-rpi.yaml @@ -0,0 +1,96 @@ + +components: + - type: generalpurpose + name: homenet + net: homenet + + - type: generalpurpose + name: rpi + ssh: odrv + net: homenet + components: + - type: uart + name: uart0 + port: /dev/ttyS0 + connected-to: main_uart + - type: can + name: can0 + interface: can0 + connected-to: odrive.can + # need to specify GPIOs explicitly for the generalpurpose type + - {type: gpio, num: 16} + - {type: gpio, num: 19} + - {type: gpio, num: 20} + - {type: gpio, num: 26} + +# - type: programmer +# name: The Blue STLink/v2 +# id: '\x53\x3f\x75\x06\x49\x3f\x49\x51\x44\x54\x19\x3f' + + - type: odrive + name: odrive + board-version: v3.6-58V + serial-number: "20703595524B" + brake-resistance: 0.47 + usb: auto + can: main_canbus + vbus-voltage: 24 # [V] + max-brake-power: 150 # [W] + encoder0: virtual_encoder0 + encoder1: virtual_encoder1 + motor0: D5065-270KV_0 + motor1: floating + + - type: motor + name: D5065-270KV_0 + phase-resistance: 0.039 + phase-inductance: 1.57e-05 + pole-pairs: 7 + direction: 1 + kv: 270 + max-current: 70 + max-voltage: 40 + + - type: encoder + name: real_encoder + cpr: 8192 + max-rpm: 7000 + + - type: teensy + name: teensy + + - {type: lpf, name: lpf0} + - {type: lpf, name: lpf1} + +connections: + - ['odrive.can', 'rpi.can0'] + - ['teensy.program', 'rpi.gpio26'] + - ['teensy.gpio12', 'rpi.uart0.tx'] + - ['teensy.gpio13', 'rpi.uart0.rx'] + - ['teensy.gpio11', 'odrive.gpio1'] + - ['teensy.gpio10', 'odrive.gpio2'] + - ['teensy.gpio9', 'odrive.gpio3'] + - ['teensy.gpio8', 'odrive.gpio4'] + - ['teensy.gpio14', 'odrive.gpio5'] + - ['teensy.gpio15', 'odrive.gpio6'] + - ['teensy.gpio16', 'odrive.gpio7'] + - ['teensy.gpio17', 'odrive.gpio8'] + - ['teensy.gpio6', 'rpi.gpio20'] + - ['teensy.gpio7', 'rpi.gpio19'] + - ['teensy.gpio23', 'odrive.encoder0.z'] + - ['teensy.gpio22', 'odrive.encoder0.b'] + - ['teensy.gpio21', 'odrive.encoder0.a'] + - ['teensy.gpio20', 'odrive.encoder1.z'] + - ['teensy.gpio19', 'odrive.encoder1.b'] + - ['teensy.gpio18', 'odrive.encoder1.a'] + - ['teensy.gpio0', 'real_encoder.z'] + - ['teensy.gpio1', 'real_encoder.a'] + - ['teensy.gpio2', 'real_encoder.b'] + - ['teensy.gpio3', 'odrive.spi.mosi'] + - ['teensy.gpio4', 'odrive.spi.miso'] + - ['teensy.gpio5', 'odrive.spi.sck'] + - ['odrive.axis0', 'D5065-270KV_0'] + - ['D5065-270KV_0', 'real_encoder'] + - ['odrive.gpio3', 'lpf0'] + - ['odrive.gpio4', 'lpf1'] + - ['lpf0.en', 'lpf1.en', 'rpi.gpio16']